Skip to main content

Find every recording for one Teams meeting

This is a step-by-step example, not a high-level pattern. It starts from a single Microsoft Teams recording and ends with every recording that belongs to the same meeting — plus their transcripts and audio.

Every step is a curl call you can paste into a terminal. $BASE_URL is your regional API host (for example https://uk.focus.subphonic.ai/api — see Base URLs), and $TOKEN is a bearer token from POST /authWithApiKey.

Two things group recordings here, and both matter: an Event ID ties together every participant's recording of the same conversation, and a linked interaction ties together recordings around a single leg — a transfer, a conference leg, or the same person rejoining. You filter on the first; the second comes back in your results automatically (Step 4).

When to use this

Use it whenever "one recording" is not the unit you actually care about. Compliance review, eDiscovery export and QA of a multi-party meeting all need the conversation, not one participant's leg of it.

Why Teams needs this​

Focus records Microsoft Teams per participant. A meeting with four recorded participants produces four separate recordings, each with its own interactionId, its own thirdPartyId, its own media file and its own transcript.

Search by person, endpoint or date range and you get one of those legs. What ties them together is the Event ID — a single value carried by every recording in the conversation.

The same identifier appears under three names:

Where you see itName
Focus UI, and the ?eventId= deep linkEvent ID
Focus REST API, and the attributes bag on a recordingscenarioId
Microsoft TeamsCall Chain ID

They are the same value. The API uses scenarioId.

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)

The service user needs Recording.View for the searches below, plus Recording.Transcription.View and Recording.Play for steps 5 and 6.

Step 2 — Find one leg of the meeting​

Start from whatever you already know — here, Teams communications for one person on the day of the meeting:

curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"personIds": { "include": [42] },
"communicationTypes": { "include": ["teams"] },
"dateTimeRange": {
"from": "2026-09-08T00:00:00Z",
"to": "2026-09-09T00:00:00Z"
},
"startTimeDirection": "DESC",
"page": 1,
"pageSize": 100
}'

Each result carries its Event ID in the attributes bag:

{
"recordings": [
{
"interactionId": 4815162,
"thirdPartyId": "REC-00123",
"startDatetimeUTC": "2026-09-08T14:02:11Z",
"durationInSeconds": 742,
"type": "teams",
"attributes": {
"scenarioId": "cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62",
"platform": "teams"
}
}
],
"total": 1,
"page": 1,
"pageSize": 100
}

Step 3 — Read the Event ID​

attributes is omitted entirely when a recording has none, so read it defensively rather than assuming the key is there:

EVENT_ID=$(curl -s -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "recordingIds": { "include": [4815162] } }' \
| jq -r '.recordings[0].attributes.scenarioId // empty')

echo "$EVENT_ID"
# cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62

An empty $EVENT_ID means this recording has no Event ID — treat it as a standalone communication rather than retrying with a partial value.

Step 4 — Retrieve every leg of the conversation​

Filter on the Event ID and you get the whole meeting back:

curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"attributes\": { \"scenarioId\": \"$EVENT_ID\" },
\"startTimeDirection\": \"ASC\",
\"page\": 1,
\"pageSize\": 50
}"
{
"recordings": [
{
"interactionId": 4815162,
"thirdPartyId": "REC-00123",
"startDatetimeUTC": "2026-09-08T14:02:11Z",
"durationInSeconds": 742,
"attributes": {
"scenarioId": "cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62",
"platform": "teams"
}
}
],
"total": 4,
"page": 1,
"pageSize": 50
}

total: 4 — one recording per recorded participant. The response above is abridged to a single entry; the other three legs have the same shape, the same scenarioId, and their own interactionId and thirdPartyId.

Only the legs you are allowed to see

The search is still filtered by your service user's roles and rights. A four-participant meeting can legitimately return fewer than four rows if some participants are outside what that user may access — so treat total as "legs visible to me", not "legs recorded".

Linked legs come back automatically

You don't have to ask for linked recordings. Where a matched recording is part of a linked interaction — a transfer, a conference leg, or the same person leaving and rejoining the meeting — the other members are returned in the same result set. Any row added that way carries "includedAsSibling": true, meaning it didn't match your filter itself:

{
"interactionId": 4815166,
"includedAsSibling": true,
"interaction": { "groupType": "rejoin", "groupId": "a1b2c3d4" }
}

interaction.groupType is one of transfer, conference, rejoin, sms_thread, cross_channel or manual — a participant who rejoined is the rejoin case. groupId is an opaque id shared by the members of that group.

Two different groupings are in play here, and they answer different questions:

FieldGroupsExample
attributes.scenarioId (Event ID)every recording in one conversationfour participants in one meeting
interaction.groupIdlinked recordings around one legone participant who dropped out and rejoined

interaction is populated only when the recording is part of a linked interaction and at least one other member is visible to you — so treat a missing interaction as "no linked legs I can see", not "no linked legs".

Collect the ids for the next two steps:

LEGS=$(curl -s -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"attributes\": { \"scenarioId\": \"$EVENT_ID\" }, \"page\": 1, \"pageSize\": 50 }")

echo "$LEGS" | jq '[.recordings[].interactionId]' # [4815162, 4815163, 4815164, 4815165]
echo "$LEGS" | jq -r '.recordings[].thirdPartyId' # REC-00123, REC-00124, …

Step 5 — Fetch the transcript for every leg​

Transcription takes the numeric ids, and accepts all of them in one call:

curl -X POST "$BASE_URL/client/recording.transcription.get" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "interactionIds": [4815162, 4815163, 4815164, 4815165] }'

Each leg is transcribed independently, so you get one entry per recording — a per-participant view of the same meeting. An empty or null analysis means that leg has not been transcribed.

Step 6 — Download the media for a leg​

Media download is keyed on thirdPartyId, one call per leg:

curl -X POST "$BASE_URL/client/recording.playBackByThirdPartyId" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "thirdPartyId": "REC-00123", "format": "mp4" }' \
--output leg-1.mp4

Use mp4 for Teams meetings with video and mp3 for audio only. If your account has forced comments enabled, include a comment explaining the access or the call returns 400.

Step 7 — Open the same conversation in the UI​

The portal takes the same value as a deep link, which is the quickest way to sanity-check what your integration retrieved:

https://uk.focus.subphonic.ai/communications?eventId=cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62

How attribute matching behaves​

The attributes filter is deliberately strict:

  • Exact match only — no partial or prefix matching.
  • Multiple keys are ANDed.
  • Types are preserved — numeric 3 does not match string "3".
  • One value per key — to search several Event IDs, issue one request each.
  • Ten keys maximum.

Arrays, objects and null return 400 rather than being quietly ignored, as does an eleventh key. An empty string is a real filter, so {"scenarioId": ""} matches nothing rather than everything.

Practical guidance​

  • Read attributes defensively. The bag is omitted when a recording has none, and scenarioId is absent for sources that don't supply one. Fall back to treating the recording as standalone.
  • Store the Event ID alongside your own records. If you sync recordings into your own store, persisting scenarioId lets you group legs locally without re-querying Focus.
  • One request per Event ID. The filter takes a single value per key, so a batch of conversations is a loop, not one call. Space the requests out to stay inside your rate limit.
  • Set pageSize explicitly. Large meetings and long transfer chains can produce more legs than you expect; page through them rather than assuming one page is the whole conversation.
  • Don't build the id yourself. Always take scenarioId from a recording or an Event ID a user gave you. It is opaque, and partial values match nothing.
Docs AssistantAsk anything about our products