Skip to main content

Endpoint Reference

This is the per‑endpoint reference for the read‑only surface of the Native Focus REST API. Every call below requires a bearer token from POST /authWithApiKey — see Getting Started → Authentication.

Read‑only

Only GET and search‑style POST endpoints are listed. Write, update and delete operations exist on the API but are not documented publicly. Contact support if your use case needs one.

Prefer to try calls live?

This page is a curated reference with worked examples. For the machine‑generated schema of any endpoint — every field typed — and an interactive "Try it" console, open the Focus API (OpenAPI) reference. Each endpoint below links to its OpenAPI page.

All paths are relative to your tenant's {{baseUrl}} (see Base URLs). All authenticated calls require:

Authorization: Bearer YOUR.BEARER.TOKEN
Content-Type: application/json

Authentication​

Authenticate with API key​

POST /authWithApiKey · Try it live ↗

Exchange an API key for a short‑lived bearer JWT.

Body

{
"clientId": 1234567,
"apiKey": "YOUR-API-KEY-XYZ",
"username": "YOUR-INTEGRATION-USERNAME"
}
FieldTypeRequiredNotes
clientIdintegeryesYour Focus account id.
apiKeystringyesAPI key generated in the admin portal.
usernamestringyesAuto‑generated integration username shown next to the API key in admin. The token inherits this user's roles & rights.

Response

{
"token": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi..."
}

When token expires, call this endpoint again with the same body to get a fresh one.


Recordings​

The "recording" resource represents a single communication — a phone call, Teams meeting, SMS message, etc. Most endpoints accept either the numeric interactionId (a.k.a. recordingId) or the source‑system thirdPartyId depending on what they do.

Search recordings​

POST /client/recording.search · Try it live ↗

Search and filter recordings. This is the workhorse endpoint for any integration that needs to find calls.

Required right: Recording.View

Body — every field is optional; combine them to narrow your result set.

FieldTypeNotes
recordingIds{ include?: number[], exclude?: number[] }Filter by internal ids.
thirdPartyIds{ include?: string[], exclude?: string[] }Filter by source ids.
attributes{ [key]: string | number | boolean }Exact‑match filter on the recording's attributes bag — including scenarioId, the Event ID shared by every recording in a conversation.
endpointIds{ include?, exclude? }Filter by endpoint (phone/email/Teams id) record.
personIds{ include?, exclude? }Filter by person record.
userIds{ include?, exclude? }Calls owned by these Focus users.
dateTimeRange{ from: ISO8601, to: ISO8601 }Most common filter; use a tight window when paging.
durationRangeInSeconds{ min?: number, max?: number }
timeRanges{ include?, exclude? }Time‑of‑day filters.
communicationsBetweenobjectFilter to interactions between two specific endpoint sets.
freeTextstringSearches transcript / metadata.
communicationTypes{ include?: string[], exclude?: string[] }e.g. phone, teams, sms.
direction"In" | "Out"
tagIds{ include?, exclude? }
isPrivateboolean
hasCommentsboolean
legalHoldIdintegerRestrict to a legal hold.
playlistIdintegerRestrict to a playlist.
startTimeDirection"ASC" | "DESC"Sort direction.
page, pageSizenumberPagination.

Example request

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

Example response (abridged)

{
"recordings": [
{
"interactionId": 9876543,
"thirdPartyId": "6414aa9f-ZZZAAABBB",
"startDatetimeUTC": "2026-05-12T10:30:00.000Z",
"endDatetimeUTC": "2026-05-12T10:34:21.482Z",
"durationInSeconds": 261.482,
"type": "phone",
"endpoints": [
{ "endpoint": { "id": 81, "type": "phone", "endpoint": "447896423595" } }
]
}
],
"total": 4321,
"page": 1,
"pageSize": 100
}
Full example response

Fields below match the Interaction shape returned by the API. The exact contents of analysis, tags, dataGroups and optional depend on what your account has configured; the values shown are illustrative.

{
"recordings": [
{
"interactionId": 9876543,
"thirdPartyId": "6414aa9f-ZZZAAABBB",
"startDatetimeUTC": "2026-05-12T10:30:00.000Z",
"endDatetimeUTC": "2026-05-12T10:34:21.482Z",
"durationInSeconds": 261.482,
"type": "phone",
"source": "teams",
"commentsCount": 1,
"transcriptSnippet": "The call focused on resetting the customer's online banking credentials…",
"analysis": ["Customer Service"],
"tags": ["verified_id"],
"conversationPhases": ["greeting", "resolution", "closing"],
"isPrivate": false,
"isPrivateReason": "",
"mediaAccessStatus": "Permitted",
"retentionGroupId": 12,
"retainUntil": "2033-05-12T10:34:21.482Z",
"legalHolds": [],
"dataGroups": { "Department": "Support" },
"optional": {},
"endpoints": [
{
"isOriginator": true,
"isRecordingSubject": true,
"user": "jane.smith@example.com",
"endpoint": { "id": 81, "type": "phone", "endpoint": "447896423595" },
"person": { "id": 42, "fullName": "Jane Smith", "email": "jane.smith@example.com" }
},
{
"isOriginator": false,
"isRecordingSubject": false,
"user": "",
"endpoint": { "id": 9123, "type": "phone", "endpoint": "447700900111" },
"person": { "id": null, "fullName": "", "email": null }
}
]
}
],
"total": 4321,
"page": 1,
"pageSize": 100,
"recordingsSearchToken": "st_01HQ1WHV01BVANN5NW4PG3N573"
}

POST /client/recording.search · Try it live ↗

An Event ID identifies every communication that belongs to the same conversation. A Microsoft Teams meeting recorded for four participants produces four separate recordings in Focus, all sharing one Event ID — so searching on it is how you retrieve a whole conversation rather than a single leg of it.

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.

Required right: Recording.View

Example request

curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attributes": { "scenarioId": "cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62" }
}'

Example response (abridged)

{
"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
}

attributes is omitted entirely when a recording has none — read it defensively.

Combining keys

Keys are ANDed, so a second attribute narrows the same search further — here, the Teams leg(s) of that conversation only:

curl -X POST "$BASE_URL/client/recording.search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"scenarioId": "cc0da036-2f1e-4b7a-9d33-8a1c5e0f4b62",
"platform": "teams"
}
}'

How attribute matching behaves​

  • 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.

Opening the same conversation in the UI​

Paste the Event ID into the Focus portal as a deep link to see the same set of recordings a user would:

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

Find every recording for one Teams meeting takes this from one participant's recording to every leg of the meeting, with its transcript and audio.


Get recording detail​

POST /client/recording.detail.get · Try it live ↗

Fetch the full detail blob for one or more recordings (metadata, endpoints, owners, retention info, encryption metadata).

Required right: Recording.View

Body

{ "interactionIds": [9876543, 9876544] }

Returns an array of detail objects keyed on interactionId. Each entry carries the underlying metadata, audio waveForm summary and encryptionDetail blocks for the recording. Use this after recording.search when you need raw metadata that isn't included in the search projection.

Full example response

The metadata, waveForm and encryptionDetail blocks are passthroughs from the underlying recording pipeline — the exact contents vary by communication source (Teams, telephony, SMS, etc.). The example below is illustrative.

[
{
"interactionId": 9876543,
"metadata": {
"accountGuid": "d6b8…",
"recorder": "teams-bot",
"region": "uk",
"sourceFileDetail": {
"mp3": { "fileSize": 4189312 },
"wav": { "fileSize": 41893120 }
}
},
"waveForm": {
"sampleRate": 8,
"peaks": [0.01, 0.42, 0.55, 0.31, 0.18, 0.0]
},
"encryptionDetail": {
"algorithm": "AES-256-GCM",
"keyId": "kek-2026-05"
}
}
]

Get transcription​

POST /client/recording.transcription.get · Try it live ↗

Fetch transcription segments for one or more recordings.

Required right: Recording.Transcription.View

Body

{ "interactionIds": [9876543] }

The response is an array of { interactionId, analysis } objects. The analysis field is the raw output written by the transcription engine for that interaction — typically a transcript containing time‑aligned segments plus a detected language. An empty or null analysis means transcription is disabled, hasn't run yet, or isn't available for that media type.

Full example response

Real payload taken from a call processed by the Focus transcription pipeline. Segment timings are in seconds (floating point); speaker diarisation is not currently included in the public response.

[
{
"interactionId": 9876543,
"analysis": {
"transcript": {
"language": "en",
"segments": [
{
"text": " Hello. Thank you for holding me on. How can I help? Hi, yeah. I want to know what the progress was with my fellow installation…",
"start": 2.585,
"end": 28.933
},
{
"text": " Perfect, it stays like its waiting an install date, so what I can do is Ill pop you over to the install team…",
"start": 30.435,
"end": 45.111
}
]
}
}
}
]

Get SMS message​

POST /client/recording.sms-message.get · Try it live ↗

Fetch the message body for SMS interactions.

Required right: Recording.View

Body

{ "interactionIds": [9876543] }

Use this for type: "sms" interactions where the "audio" is actually a text payload. The response projects the SMS body and any underlying provider message ids out of the recording's metadata.

Full example response
[
{
"interactionId": 9876543,
"message": "Hi — your appointment is confirmed for 14 May at 10:30. Reply STOP to opt out.",
"rawSmsIds": ["sms-7c2f-001"]
}
]

Download audio​

POST /client/recording.playBackByThirdPartyId · Try it live ↗

Stream or download the audio (or video) for a recording. Returns a binary stream — pipe to a file.

This is both the playback and the download path — there is no separate download endpoint. The response body is the media itself, returned with Content-Disposition: attachment, so write it to a file rather than expecting a URL. Because the call is a POST carrying a bearer token it cannot be used as a shareable download link; to share a recording with another Focus user, use a Playlist.

Required right: Recording.Play

Body

FieldTypeRequiredNotes
thirdPartyIdstringyesSource‑system id taken from a recording.search result.
formatstringnoOutput format — see Formats below. Always set this explicitly.
streamThroughbooleannoStream as the file is decrypted instead of buffering.
commentstringconditionalRequired when the account has forced comments enabled.

Formats​

formatOutputChannels
mp3-monoMP31 (downmixed)
wav-monoWAV1 (downmixed)
wavWAV, pcm_s16leAs recorded
rawWAV container, codec copy — no transcodeAs recorded
mp4AAC 128 kbps in a fragmented MP4, audio only — no video trackAs recorded
mp3Default. Does not currently return MP3 — see the warning below2

The two mono formats perform a true downmix, not a channel drop. Compliance recordings are typically two‑channel with one party per channel; asking for mp3-mono or wav-mono mixes both parties into the single channel, so neither side of the conversation is lost. Pick a mono format when you want a smaller file or when the system consuming the audio expects one channel; pick wav or raw when you need the parties kept on separate channels, for example for diarisation or per‑party analysis.

raw is the fastest and the only lossless option, because the audio is copied rather than re‑encoded.

Always set format explicitly

format defaults to mp3, but that value does not currently produce an MP3 — it returns a stereo WAV file. For MP3 output ask for mp3-mono; there is no stereo MP3 option. For a single‑channel WAV ask for wav-mono.

Values outside the list above are not rejected. A near miss such as mono or wav_mono returns stereo WAV with a 200 rather than a 400 ValidationError, so a typo fails quietly — check the file you get back if you are unsure the value was accepted.

Example

Download a single‑channel MP3 with both parties mixed together:

curl -X POST "$BASE_URL/client/recording.playBackByThirdPartyId" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "thirdPartyId": "abc-123-call-id", "format": "mp3-mono" }' \
--output recording.mp3

Returns 404 if the thirdPartyId is unknown or not visible to the calling user.

Response details

The response is a binary stream, not JSON. Useful response headers:

HTTP/1.1 200 OK
Content-Type: audio/mpeg
Content-Disposition: attachment; filename="6414aa9f-ZZZAAABBB.mp3"
Content-Length: 4189312

Content-Type follows the requested format (audio/mpeg for mp3/mp3-mono, audio/wav for wav/wav-mono, video/mp4 for mp4, application/octet-stream for raw).


Get recording audit log​

POST /client/recording/audit · Try it live ↗

Read the access audit log for a single recording — every play, download or detail view, with user and timestamp.

Required right: Recording.Audit.View

Body

{ "recordingId": 9876543, "page": 1, "pageSize": 100 }

Use this to evidence who has accessed a particular interaction (e.g. for a regulator request). The response is an array of audit rows; each row describes one HTTP call against this recording.

See Assemble an evidence pack for a compliance request for the full flow — scoping, the access trail, and what belongs in the pack.

Full example response

Fields below match the AuditSearchResult shape. metadata is a freeform object recorded by the API at the time of the action and varies by route.

[
{
"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,
"parentEntityId": 9876543,
"entityName": "Recording",
"parentEntityName": "Recording",
"metadata": { "comment": "QA review for ticket #1234", "format": "mp3-mono" },
"responseCode": 200
},
{
"auditHeaderId": 5513,
"method": "POST",
"route": "/client/recording.detail.get",
"description": "Get recording detail",
"username": "compliance-svc",
"insertedAt": "2026-05-12T11:05:01.000Z",
"ipAddress": "10.20.30.40",
"entityId": 9876543,
"parentEntityId": 9876543,
"entityName": "Recording",
"parentEntityName": "Recording",
"metadata": null,
"responseCode": 200
}
]

People & Endpoints​

A person is an individual tracked in your Focus tenant; an endpoint is one of their addressable identities (a phone number, email or Teams user id). Endpoints are owned by people, and recordings are linked to endpoints.

POST /client/people.search · Try it live ↗

Search people with rich filters.

Required right: People.View

For a worked use of these filters, see Reconcile people and endpoints.

Body

FieldTypeNotes
freeTextstringFree‑text match across name / metadata.
personType"staff" | "external"
personIdsnumber[]Restrict to specific people.
thirdPartyIdsstring[]Restrict by source id.
emailsstring[]Restrict by email endpoint.
groupAssignmentarrayFilter by auto‑group rules.
retentionGroupIdnumber | nullFilter by retention group; null means "no group".
byoStorageAccountIdnumber | nullFilter by BYO storage account.
includeInteractionsbooleanIf true, include recent interaction stats per person.
onlyReturnNonAssociatedbooleanOnly people without an associated user.
isActiveboolean
page, pageSizenumber
Full example response
{
"persons": [
{
"id": 42,
"fullName": "Jane Smith",
"email": "jane.smith@example.com",
"thirdPartyId": "AAD-1f2c",
"personType": "staff",
"isActive": true,
"avatar": "https://cdn.example.com/avatars/42.png",
"retentionGroupId": 12,
"mostRecentCommAt": "2026-05-12T10:34:21.482Z",
"endpoints": [
{ "id": 81, "type": "phone", "endpoint": "447896423595", "ownedByPersonId": 42, "isRecorded": true },
{ "id": 82, "type": "email", "endpoint": "jane.smith@example.com", "ownedByPersonId": 42, "isRecorded": false }
],
"autoGroupAssignment": [
{ "groupName": "Department", "groupValue": "Support", "groupNameId": 3, "groupValueId": 17 }
],
"metadata": { "additionalFields": { "costCentre": "CC-204" } }
}
],
"total": 137,
"page": 1,
"pageSize": 25
}

Search people by name​

POST /client/people.searchByName · Try it live ↗

Lightweight prefix search — useful for type‑ahead UIs.

Required right: People.View

Body

{ "prefix": "smi", "personType": "staff", "isActive": true, "page": 1, "pageSize": 25 }

personType is required; prefix is matched against full name.

Full example response
{
"persons": [
{ "id": 42, "fullName": "Jane Smith", "personType": "staff", "isActive": true },
{ "id": 58, "fullName": "Jamie Smithson", "personType": "staff", "isActive": true }
],
"total": 2,
"page": 1,
"pageSize": 25
}

Get people by id​

POST /client/people.get · Try it live ↗

Fetch full person records by id.

Required right: People.View

Body — a JSON array of ids (numeric or string thirdPartyId):

[1, 2, 3]

Response (abridged)

[
{ "id": 1, "fullName": "Jane Smith", "personType": "staff" }
]
Full example response
[
{
"id": 1,
"fullName": "Jane Smith",
"email": "jane.smith@example.com",
"thirdPartyId": "AAD-1f2c",
"personType": "staff",
"isActive": true,
"avatar": "https://cdn.example.com/avatars/1.png",
"retentionGroupId": 12,
"mostRecentCommAt": "2026-05-12T10:34:21.482Z",
"endpoints": [
{ "id": 81, "type": "phone", "endpoint": "447896423595", "ownedByPersonId": 1, "isRecorded": true },
{ "id": 82, "type": "email", "endpoint": "jane.smith@example.com", "ownedByPersonId": 1, "isRecorded": false }
],
"autoGroupAssignment": [
{ "groupName": "Department", "groupValue": "Support", "groupNameId": 3, "groupValueId": 17 }
],
"metadata": { "additionalFields": { "costCentre": "CC-204" } }
}
]

POST /client/people/endpoint/search · Try it live ↗

Find endpoints (phone numbers, emails, Teams user ids) and the people that own them.

Required right: Endpoints.View

Body

FieldTypeNotes
type"phone" | "email" | "teamsUserId"
endpointSearchstringExact / partial match on the endpoint value.
freeTextstringFree‑text match.
isRecordedbooleanEndpoint has ever been recorded.
isCurrentlyRecordedbooleanRecording is currently active.
onlyReturnOwned / onlyReturnNonOwnedboolean
page, pageSizenumber

Example

curl -X POST "$BASE_URL/client/people/endpoint/search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "type": "phone", "endpointSearch": "447896" }'
Full example response

Fields below match the RecordedEndpoint shape returned by the API. The handler returns a plain JSON array — there is no total / page wrapper. inflightCall is populated only while a live call is in progress on the endpoint.

[
{
"id": 81,
"type": "phone",
"endpoint": "447896423595",
"metadata": { "label": "Support desk" },
"ownedByFullName": "Jane Smith",
"isPrivate": false,
"isCurrentlyRecorded": true,
"isRecorded": true,
"inflightCall": null,
"mostRecentCommAt": "2026-05-12T10:34:21.482Z"
},
{
"id": 92,
"type": "phone",
"endpoint": "447896000999",
"metadata": {},
"ownedByFullName": "",
"isPrivate": false,
"isCurrentlyRecorded": false,
"isRecorded": false,
"inflightCall": null,
"mostRecentCommAt": null
}
]

Get endpoints by value​

POST /client/people/endpoint/get · Try it live ↗

Fetch endpoint records by exact value.

Required right: People.View

Body — an array of { type, endpoint } objects:

[
{ "type": "phone", "endpoint": "447896423595" },
{ "type": "email", "endpoint": "jane.smith@example.com" }
]

Returns the matching endpoint records including the owning person id and recording flags. The response is a plain JSON array — entries are returned in the order they're found, not necessarily the order requested.

Full example response
[
{
"id": 81,
"type": "phone",
"endpoint": "447896423595",
"ownedByPersonId": 42,
"isPrivate": false,
"isRecorded": true
},
{
"id": 82,
"type": "email",
"endpoint": "jane.smith@example.com",
"ownedByPersonId": 42,
"isPrivate": false,
"isRecorded": false
}
]

Users​

POST /client/user/search · Try it live ↗

Search Focus portal users.

Required right: Users.View

Body — all fields optional:

FieldTypeNotes
userIdintegerSingle‑user lookup.
usernamestringExact username match.
freeTextstringFree‑text match across user fields.
rolesAndRightsstring[]Filter to users with these rights.
isActive, isLockedboolean
onlyReturnNonAssociatedbooleanUsers with no person record.
searchOrderPropertyobjectSort options.
page, pageSizenumber

Example response

[
{ "id": 42, "username": "alice@example.com", "rolesAndRights": ["Admin"] }
]
Full example response
{
"users": [
{
"id": 42,
"fullName": "Alice Adams",
"username": "alice@example.com",
"email": "alice@example.com",
"externalId": "AAD-9f3a",
"personId": 1042,
"clientId": 1234567,
"isActive": true,
"isLocked": false,
"sso": true,
"lastLoginTime": "2026-05-12T08:14:00.000Z",
"rolesAndRights": ["Admin", "Recording.View", "Recording.Play"],
"policies": ["default-retention"],
"audioPlayback": "streaming"
}
],
"total": 1,
"page": 1,
"pageSize": 1000
}

Retention Groups​

List retention groups​

GET /admin/retentionGroup · Try it live ↗

List all retention groups configured on the account, including their default status and assigned people counts.

Required right: RetentionGroup.View

curl -H "Authorization: Bearer $TOKEN" "$BASE_URL/admin/retentionGroup"
Full example response
{
"accountDefault": {
"default": 2555,
"phone": 2555,
"teams": 2555,
"sms": 1095
},
"retentionGroups": [
{
"id": 12,
"name": "Standard 7 years",
"retentionGroup": { "default": 2555, "phone": 2555, "teams": 2555, "sms": 1095 },
"peopleIds": [42, 58, 91]
},
{
"id": 13,
"name": "Short 90 days (training)",
"retentionGroup": { "default": 90 },
"peopleIds": [120, 121]
}
]
}

Duration values are expressed in days. accountDefault applies to anyone not in a specific group.


Comments​

Get comments on a recording​

GET /client/recording/comment · Try it live ↗

List comments attached to a recording.

Required right: Recording.Comment.View

Query parameters

ParamRequiredNotes
recordingIdyesInternal recording id.
bIDnoOptional bookmark id filter.
cIdfnoOptional comment‑identifier filter.

Example

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/client/recording/comment?recordingId=9876543"

Comments may include start/end offsets within the recording, a tag and the inserting user.

Full example response

Fields below match the CommentOut shape. tag is omitted when the comment isn't tagged.

[
{
"comment": "Customer ID verified at 00:35.",
"startOffsetInSeconds": 35,
"endOffsetInSeconds": 42,
"recordingId": 9876543,
"insertedByUserId": 42,
"insertedByUsername": "jane.smith@example.com",
"insertedAt": "2026-05-12T10:36:00.000Z",
"tag": { "id": 3, "tag": "verified_id" }
},
{
"comment": "Reviewed for QA — no issues.",
"startOffsetInSeconds": 0,
"recordingId": 9876543,
"insertedByUserId": 7,
"insertedByUsername": "compliance-svc",
"insertedAt": "2026-05-12T11:05:00.000Z"
}
]

Docs AssistantAsk anything about our products