Assemble an evidence pack for a compliance request
A regulator, a legal team or an internal investigation asks for the communications relating to a customer, a date range or a dispute — and for proof of who has touched them since. This example walks that request end to end.
Every step is a curl call. $BASE_URL is your regional API host (see Base URLs) and $TOKEN is a bearer token from POST /authWithApiKey.
The request has two halves, and the second is the one people forget: the communications themselves, and the access trail proving who played, downloaded or viewed each one. The audit log is the only place that second half exists.
Use it for any request where the answer has to stand up to scrutiny — a regulator request, an eDiscovery production, a subject access request, or an internal investigation into who accessed what.
Rights your service user needs
| Right | For |
|---|---|
Recording.View | Finding the communications in scope |
Recording.Audit.View | Reading the access log for each one |
Recording.Transcription.View | Including transcripts |
Recording.Play | Including media |
A missing right returns 403 on that step alone — the rest of the pack still assembles, so handle it per step rather than aborting.
Step 1 — Authenticate
TOKEN=$(curl -s -X POST "$BASE_URL/authWithApiKey" \
-H "Content-Type: application/json" \
-d '{"clientId":1234567,"apiKey":"YOUR-API-KEY-XYZ","username":"YOUR-INTEGRATION-USERNAME"}' \
| jq -r .token)
Step 2 — Scope the request
How you scope it matters more than it looks. A date-range search re-run next month can return a different set as data ages out; a legal hold is a fixed, named set that cannot be deleted from underneath you.
If the request is already under legal hold, scope to the hold:
curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "legalHoldId": 7, "page": 1, "pageSize": 100 }'
Otherwise scope by the parties and dates in the request:
curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"communicationsBetween": { "fromPersonId": 42, "toEndpointId": 9123 },
"dateTimeRange": {
"from": "2026-03-01T00:00:00Z",
"to": "2026-04-01T00:00:00Z"
},
"startTimeDirection": "ASC",
"page": 1,
"pageSize": 100
}'
Each search response carries a recordingsSearchToken — an opaque id for that search execution, used for auditing. Keep it with the pack: it is how you later evidence which search produced this set of recordings.
Step 3 — Check what you can actually include
Before assembling anything, read three fields on every result. They decide what the pack can contain:
{
"interactionId": 9876543,
"mediaAccessStatus": "RetentionExpired",
"retainUntil": "2026-02-11T09:14:00.000Z",
"isPrivate": false,
"legalHolds": [{ "id": 7, "name": "Ombudsman ref 44120" }]
}
| Field | Meaning for the pack |
|---|---|
mediaAccessStatus | Permitted — media can be pulled. PrivateDenied — the recording is marked private and this user may not play it. RetentionExpired — the media is gone; metadata may remain. |
retainUntil | When the media is due for deletion. Anything close to now is worth pulling first. |
legalHolds | The holds protecting this recording from deletion, with their names. |
A recording whose media has expired is still evidence — of the communication, and of its retention decision. Report it in the pack as metadata with an explicit note, rather than dropping it silently and producing a pack with unexplained gaps.
Step 4 — Pull the access trail for each recording
This is the half that only the audit log can answer: who has accessed this recording. One call per recording, keyed on the numeric id:
curl -X POST "$BASE_URL/client/recording/audit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "recordingId": 9876543, "page": 1, "pageSize": 100 }'
The response is a plain array — one row per HTTP call made against that recording, covering plays, downloads and detail views:
[
{
"auditHeaderId": 5512,
"method": "POST",
"route": "/client/recording.playBackByThirdPartyId",
"description": "Play recording",
"username": "jane.smith@example.com",
"insertedAt": "2026-05-12T11:02:14.000Z",
"ipAddress": "203.0.113.10",
"entityId": 9876543,
"entityName": "Recording",
"metadata": { "comment": "QA review for ticket #1234", "format": "mp3-mono" },
"responseCode": 200
},
{
"auditHeaderId": 5514,
"method": "POST",
"route": "/client/recording.playBackByThirdPartyId",
"description": "Play recording",
"username": "contractor-svc",
"insertedAt": "2026-05-14T08:41:02.000Z",
"ipAddress": "198.51.100.7",
"entityId": 9876543,
"entityName": "Recording",
"metadata": null,
"responseCode": 403
}
]
Three things in that response are worth calling out:
responseCoderecords attempts, not just successes. The403row above is someone who tried to play the recording and was refused. For an investigation into attempted access, that row is the finding.metadatavaries byroute, and carries whatever the API captured at the time. On a download from an account with forced comments enabled, the user's justification lands inmetadata.comment— often the most useful line in the whole pack.usernameis who acted, including service users. Your own integration's calls are audited the same way, so pulling a pack leaves its own trail.
Page with page / pageSize — a heavily reviewed recording can have a long history.
Step 5 — Collect content for the recordings in scope
Transcripts take every id at once:
curl -X POST "$BASE_URL/client/recording.transcription.get" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "interactionIds": [9876543, 9876544] }'
Media is one call per recording, keyed on thirdPartyId, and only where mediaAccessStatus is Permitted:
curl -X POST "$BASE_URL/client/recording.playBackByThirdPartyId" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"thirdPartyId": "6414aa9f-ZZZAAABBB",
"format": "mp3-mono",
"comment": "Ombudsman ref 44120 — evidence pack"
}' \
--output 9876543.mp3
Pass comment whether or not forced comments are enabled: it is required when they are, and when they aren't it still writes your reason into the audit trail for whoever reads it next.
Comments left by reviewers are part of the record too:
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/client/recording/comment?recordingId=9876543"
What ends up in the pack
| Item | Source |
|---|---|
| The communications in scope, with parties, times and duration | recording.search |
| How the set was selected | your filter, plus the recordingsSearchToken |
| Retention and hold position for each one | retainUntil, legalHolds, mediaAccessStatus |
| Who accessed each one, when, from where, and whether they succeeded | recording/audit |
| Stated reasons for access | metadata.comment on audit rows |
| Transcripts | recording.transcription.get |
| Media, where still available | recording.playBackByThirdPartyId |
| Reviewer commentary | recording/comment |
Practical guidance
- Prefer a legal hold as the scope. A hold is a stable, named set and protects its members from deletion. A date range is a query, and queries drift as retention runs.
- Audit is per recording, so it's a loop. Space the calls out and honour
429with backoff — a large pack is a lot of sequential calls. - Pull media before retention catches up. Sort your scope by
retainUntiland take the soonest first; once media is gone, no API brings it back. - Don't paraphrase the audit log. Keep
username,insertedAt,ipAddress,routeandresponseCodeverbatim in the pack. Summaries are for the covering note, not the evidence. - Expect your own footprint. The pack you assemble appears in future audit logs as your service user. Give that user a name a reader will recognise months later.
Related
- Get recording audit log — full schema and an interactive "Try it" console.
- Find every recording for one Teams meeting — make sure a request covers the whole conversation, not one participant's leg.
- Endpoint reference — every documented call, with example responses.