Skip to main content

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.

When to use this

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​

RightFor
Recording.ViewFinding the communications in scope
Recording.Audit.ViewReading the access log for each one
Recording.Transcription.ViewIncluding transcripts
Recording.PlayIncluding 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
}'
Record how you scoped it

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" }]
}
FieldMeaning for the pack
mediaAccessStatusPermitted — 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.
retainUntilWhen the media is due for deletion. Anything close to now is worth pulling first.
legalHoldsThe 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:

  • responseCode records attempts, not just successes. The 403 row above is someone who tried to play the recording and was refused. For an investigation into attempted access, that row is the finding.
  • metadata varies by route, and carries whatever the API captured at the time. On a download from an account with forced comments enabled, the user's justification lands in metadata.comment — often the most useful line in the whole pack.
  • username is 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​

ItemSource
The communications in scope, with parties, times and durationrecording.search
How the set was selectedyour filter, plus the recordingsSearchToken
Retention and hold position for each oneretainUntil, legalHolds, mediaAccessStatus
Who accessed each one, when, from where, and whether they succeededrecording/audit
Stated reasons for accessmetadata.comment on audit rows
Transcriptsrecording.transcription.get
Media, where still availablerecording.playBackByThirdPartyId
Reviewer commentaryrecording/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 429 with backoff — a large pack is a lot of sequential calls.
  • Pull media before retention catches up. Sort your scope by retainUntil and take the soonest first; once media is gone, no API brings it back.
  • Don't paraphrase the audit log. Keep username, insertedAt, ipAddress, route and responseCode verbatim 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.
Docs AssistantAsk anything about our products