# Use with AI Source: https://aircaps.ai/docs/ai-tools Give coding assistants and agents the AirCaps docs as Markdown, llms.txt or an MCP server. Every page has a menu (top right) to **copy it as Markdown** or open it in ChatGPT, Claude, Perplexity, Cursor or VS Code. | Resource | URL | Use it for | | - | - | - | | Page as Markdown | Add `.md` to any page URL, e.g. [`/quickstart.md`](https://aircaps.ai/docs/quickstart.md) | One page in an agent's context | | Index | [`/llms.txt`](https://aircaps.ai/docs/llms.txt) | A map of every page, for agents to choose from | | Full docs | [`/llms-full.txt`](https://aircaps.ai/docs/llms-full.txt) | All docs in one file | | OpenAPI spec | [`/openapi.json`](https://aircaps.ai/docs/openapi.json) | Generating clients and tool definitions | | MCP server | `https://aircaps.ai/docs/mcp` | Letting an agent search the docs itself | ## Connect the MCP server ```bash Claude Code theme={null} claude mcp add --transport http aircaps-docs https://aircaps.ai/docs/mcp ``` ```json Cursor / VS Code theme={null} { "mcpServers": { "aircaps-docs": { "url": "https://aircaps.ai/docs/mcp" } } } ``` ## Prompt to start with ```text theme={null} Use the AirCaps AI API docs at https://aircaps.ai/docs/llms.txt. Write a script that uploads audio.mp3, transcribes it with A5S Async v1 (English) and prints each utterance with its speaker. Read my key from the AIRCAPS_API_KEY environment variable. ``` # Get account Source: https://aircaps.ai/docs/api-reference/account GET /v1/account Your usage limit, per-model access (`enabled`) and limits. For higher limits or model access, [talk to sales](https://aircaps.ai/contact). # Add an enrollment Source: https://aircaps.ai/docs/api-reference/add-enrollment POST /v1/speakers/{id}/enrollments Add one clip of this speaker. The model turns it into a speaker embedding, which is stored; the audio is not. A speaker's profile combines all of its enrollments, so several clips from different recordings make it sturdier. Synchronous: the response is the new enrollment and the updated speaker. # Create a diarization Source: https://aircaps.ai/docs/api-reference/create-diarization POST /v1/diarizations Find who spoke when with AirCaps Diarization-v1 (enabled per account). The job runs asynchronously: poll [Get a diarization](/api-reference/get-diarization) or pass `webhook_url`. # Create a speaker Source: https://aircaps.ai/docs/api-reference/create-speaker POST /v1/speakers Create a speaker to enroll (AirCaps Speaker ID-v1, enabled per account). A speaker is a name and optional metadata until you [add enrollments](/api-reference/add-enrollment). # Create a transcript Source: https://aircaps.ai/docs/api-reference/create-transcript POST /v1/transcripts Start transcribing audio with A5S Async v1. The job runs asynchronously: poll [Get a transcript](/api-reference/get-transcript) or pass `webhook_url`. # Delete a diarization Source: https://aircaps.ai/docs/api-reference/delete-diarization DELETE /v1/diarizations/{id} Cancels a `queued` or `processing` job (its `error.code` becomes `canceled`), or deletes a finished diarization and its stored data, including the uploaded file it used. Afterwards the ID returns `404`. # Delete an enrollment Source: https://aircaps.ai/docs/api-reference/delete-enrollment DELETE /v1/speakers/{id}/enrollments/{enrollment_id} Removes one enrollment's embedding, immediately and permanently. The speaker's profile is recomputed from the rest. # Delete a speaker Source: https://aircaps.ai/docs/api-reference/delete-speaker DELETE /v1/speakers/{id} Deletes the speaker and all its enrollment embeddings, immediately and permanently. Afterwards the ID returns `404`. # Delete a transcript Source: https://aircaps.ai/docs/api-reference/delete-transcript DELETE /v1/transcripts/{id} Cancels a `queued` or `processing` job (its `error.code` becomes `canceled`), or deletes a finished transcript and its stored data, including the uploaded file it used. Afterwards the ID returns `404`. # Get RTTM Source: https://aircaps.ai/docs/api-reference/diarization-rttm GET /v1/diarizations/{id}/rttm The segments of a completed diarization as an RTTM file (one `SPEAKER` line per segment), the format diarization scoring tools read. # Get a diarization Source: https://aircaps.ai/docs/api-reference/get-diarization GET /v1/diarizations/{id} Status and, once `completed`, the result. # Get a speaker Source: https://aircaps.ai/docs/api-reference/get-speaker GET /v1/speakers/{id} The speaker and its enrollments. # Get a transcript Source: https://aircaps.ai/docs/api-reference/get-transcript GET /v1/transcripts/{id} Status and, once `completed`, the result. Pass `wait` to hold the request until the transcript finishes instead of polling. # Identify a speaker Source: https://aircaps.ai/docs/api-reference/identify-speaker POST /v1/speakers/identify Score one clip against your enrolled speakers: every speaker (or the ones in `speaker_ids`), best first, with a similarity score for each speaker's profile and for each enrollment. Synchronous: the result is the response. No threshold is applied; see [Speaker ID](/guides/speaker-id#scores-and-thresholds). # List diarizations Source: https://aircaps.ai/docs/api-reference/list-diarizations GET /v1/diarizations Your diarizations, newest first. Items omit the result fields. # List speakers Source: https://aircaps.ai/docs/api-reference/list-speakers GET /v1/speakers Your enrolled speakers, newest first, with their enrollments. # List transcripts Source: https://aircaps.ai/docs/api-reference/list-transcripts GET /v1/transcripts Your transcripts, newest first. Items omit the result fields (`text`, `speakers`, `utterances`, `words`). # List models Source: https://aircaps.ai/docs/api-reference/models GET /v1/models The available models. No authentication needed. # Diarization object Source: https://aircaps.ai/docs/api-reference/objects/diarization Returned by every diarization endpoint. The result fields (num_speakers, speakers, segments, speech_duration, overlap_duration) appear only in Get a diarization, once status is completed. # Speaker object Source: https://aircaps.ai/docs/api-reference/objects/enrolled-speaker An enrolled speaker of AirCaps Speaker ID-v1: a name, metadata and enrollments. Returned by the speaker endpoints. # Error object Source: https://aircaps.ai/docs/api-reference/objects/error Every HTTP error uses this envelope. See [Errors](/errors) for all codes. # File object Source: https://aircaps.ai/docs/api-reference/objects/file An uploaded file. Use id as file_id for up to 24 hours after upload; the file is then deleted within the next 24 hours, or right away when a transcript that used it is deleted. # Segment object Source: https://aircaps.ai/docs/api-reference/objects/segment One stretch of speech by one speaker. Segments of different speakers overlap where they talk at the same time. # Speaker enrollment object Source: https://aircaps.ai/docs/api-reference/objects/speaker-enrollment One enrollment clip of a speaker. Only its speaker embedding is stored, never the audio. # Speaker identification object Source: https://aircaps.ai/docs/api-reference/objects/speaker-identification The result of Identify a speaker: every compared speaker, best first, with scores. Not stored. # Transcript object Source: https://aircaps.ai/docs/api-reference/objects/transcript Returned by every transcript endpoint. The result fields (text, speakers, utterances, words) appear only in Get a transcript, once status is completed. # Utterance object Source: https://aircaps.ai/docs/api-reference/objects/utterance One speaker turn. Times are seconds from the start of the audio. # Webhook event Source: https://aircaps.ai/docs/api-reference/objects/webhook-event The body AirCaps `POST`s to your `webhook_url`. See [Webhooks](/guides/webhooks) for signing and retries. # Word object Source: https://aircaps.ai/docs/api-reference/objects/word One word with its timing and speaker. # Overview Source: https://aircaps.ai/docs/api-reference/overview Base URL, authentication and conventions. * **Base URL:** `https://api.aircaps.com/v1` * **Auth:** `Authorization: Bearer aircaps_sk_...` on every request ([API keys](/docs/authentication)) * **Format:** JSON request and response bodies; timestamps are RFC 3339 UTC; audio times are seconds. * **IDs:** `tr_...` transcripts, `file_...` uploads, `req_...` request IDs (also in the `x-request-id` header). * **Errors:** one envelope for all errors ([Errors](/docs/errors)). * **Idempotency:** send `Idempotency-Key` with `POST /v1/transcripts` to make retries safe. * **Language:** English only today: `language_code` (batch) takes `en` or a regional English code such as `en-US`, `en-GB` or `en-IN`, and `language` (streaming) takes `en`. Multilingual support is planned. * **Pagination:** list endpoints return `has_more` and `next_before`; pass it as `before` for the next page. | Endpoint | Purpose | | - | - | | `POST /v1/transcripts` | Create a transcript | | `GET /v1/transcripts/{id}` | Get status and result | | `GET /v1/transcripts` | List transcripts | | `DELETE /v1/transcripts/{id}` | Cancel or delete | | `GET /v1/transcripts/{id}/subtitles` | SRT or VTT captions | | `POST /v1/files/upload-url` | Presigned upload (≤ 5 GB) | | `POST /v1/files` | Direct upload (≤ 1 GB) | | `WSS /v1/realtime` | Live streaming | | `GET /v1/models` | Available models | | `GET /v1/account` | Account and limits | | `GET /v1/usage` | Daily usage | **Objects:** [Transcript](/docs/api-reference/objects/transcript), [Utterance](/docs/api-reference/objects/utterance), [Word](/docs/api-reference/objects/word), [File](/docs/api-reference/objects/file), [Error](/docs/api-reference/objects/error), [Webhook event](/docs/api-reference/objects/webhook-event). The full OpenAPI 3.1 spec is at [`/openapi.json`](/docs/openapi.json). # Realtime stream Source: https://aircaps.ai/docs/api-reference/realtime Live transcription with A5S Small V2 Streaming over a WebSocket. **WSS** `wss://api.aircaps.com/v1/realtime` Requires access to A5S Small V2 Streaming. For a walkthrough, see [Live streaming](/docs/guides/streaming). ## Connection **Authorization** `string` *required* `Bearer aircaps_sk_...`, sent with the WebSocket handshake. The flow: connect, send `start`, wait for `ready`, stream binary audio, send `finish`, then receive `finished`. The server then closes with `1000`. ## Client messages ### start The first message, as a JSON text frame, within 45 seconds of connecting. Unknown fields are rejected. **type** `string` *required* `"start"` **language** `string` (default: en) Language of the audio. English only today: `en`. Multilingual support is planned. **encoding** `string` (default: pcm\_s16le) Only `pcm_s16le` (signed 16-bit little-endian PCM). **sample\_rate** `integer` (default: 16000) Only `16000`. **channels** `integer` (default: 1) Only `1` (mono). **providers** `string[]` (default: \["a5sv2"]) The streaming engine. Only `"a5sv2"`. **custom\_vocabulary\_enabled** `boolean` (default: false) Turns on key terms: biases recognition toward the entries in `custom_vocabulary`. **custom\_vocabulary** `object[]` Key terms: up to 64. See [Key terms](/docs/guides/streaming#key-terms). **properties** **text** `string` *required* A word or phrase, up to 100 characters. **strength** `number` *required* Bias strength, 0 to 12. **diarization** `boolean` (default: false) Label who said what. Each `transcript` then also carries `utterances` with a speaker on every utterance and word, and `speakers`. See [Speaker labels](/docs/guides/streaming#speaker-labels). **transcript\_text** `string` (default: full) `"full"`: every `transcript` carries the complete transcript so far in `text`. `"incremental"`: every `transcript` instead carries an edit of the text you have: `text_delete` and `text_append` (below). Use `"incremental"` for long sessions: in `"full"` mode each message grows with the transcript. **speaker\_identification** `boolean | object` (default: false) Put names on the speakers with AirCaps Speaker ID-v1 (requires access to that model, and `"diarization": true`). `true` uses the defaults. Sends a `speaker_identification` message (below) for each stretch of a speaker's speech, and adds `speaker_id` and `speaker_name` to utterances. See [Speaker names](/docs/guides/streaming#speaker-names). **properties** **threshold** `number` (default: 0.6) A speaker gets a name when their best match scores above this (0 to 1) and is clearly ahead of the next-best. **speaker\_ids** `string[]` Compare with these enrolled speakers only. Default: every enrolled speaker on the account. ```json Example theme={null} {"type": "start", "language": "en", "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1, "diarization": true, "custom_vocabulary_enabled": true, "custom_vocabulary": [{"text": "AirCaps", "strength": 6}]} ``` ### Audio Binary frames of raw PCM in the `start` format: non-empty, an even number of bytes, at most 64 KB (larger frames end the session with `frame_too_large`; frames over 1 MB close the connection without a message). Send them after `ready`, in real time; a 2-second burst is allowed. 20 ms frames (640 bytes) are recommended. ### ping `{"type": "ping"}`. The server answers `pong`. Any message, audio or `ping`, resets the 45-second idle timer. ### finish `{"type": "finish"}`. The server flushes the final transcript, sends `finished`, and closes. ## Server messages Each is a JSON text frame with a `type`. ### provider\_status Startup progress while the session starts. **message** `string` For example `"Getting things ready, please hold on"`. ### ready Start sending audio. **session\_limit\_ms** `integer` Maximum audio for this session: your session length (30 minutes unless your account has a longer one, up to 4 hours), or less if little free usage and credit remain. **transcript\_text** `string` `"full"` or `"incremental"`, as requested in `start`. **quota\_ms** `integer` The account's free usage, in milliseconds. **used\_ms** `integer` Free usage used so far, in milliseconds. **language** `string` `"en-US"` **sample\_rate** `integer` `16000` **encoding** `string` `"pcm_s16le"` **diarization** `boolean` Whether speaker labels are on for this session. **speaker\_identification** `object` Only with `speaker_identification`: `model` (`"aircaps-speaker-id-v1"`), `threshold`, `speaker_ids` (`null` for every enrolled speaker) and `speakers_compared`. **warnings** `object[]` Only when `start` named a deprecated model id (`a5s-v2-streaming`, now `a5s-small-v2-streaming`): `{code, message}` with `code` `"model_deprecated"`. The session runs normally. ### transcript **text** `string` With `transcript_text``"full"` (the default): the complete transcript so far, punctuated and cased. Replace what you display instead of appending. **text\_delete** `integer` With `transcript_text``"incremental"`: remove this many characters from the end of the text you have (recent words the engine revised; often 0). **text\_append** `string` With `"incremental"`: then append this. The result is the complete transcript so far, as `text` would have been. **is\_final** `boolean` `true` when the current segment is settled. **provider** `string` `"a5sv2"` **utterances** `object[]` Only with `diarization`. The utterances that became final since the previous `transcript` (`is_final: true`; each is sent once and never changes), followed by every pending utterance (`is_final: false`; these may still change, so replace the previous pending ones). Keep the final ones and show them with the latest pending ones, ordered by `start`. **properties** **speaker** `string | null` `speaker_0`, `speaker_1`, … in order of first speech, as in async transcripts. `null` on a pending utterance before any speaker has been recognized. **start** `number` Seconds from the start of the session. **end** `number` Seconds from the start of the session. **text** `string` Punctuated and cased. **is\_final** `boolean` `true` once the utterance will not change. **words** `object[]` `{text, start, end, speaker}` for each word. **speaker\_id** `string | null` Only with `speaker_identification`. The enrolled speaker named by the `speaker_identification` decision for this `speaker` that overlaps the utterance most, as known when it is sent; `null` until then or when unnamed. **speaker\_name** `string | null` Only with `speaker_identification`. That speaker's name. **speakers** `string[]` Only with `diarization`. The speaker labels used so far in final utterances. ```json Example theme={null} {"type": "transcript", "provider": "a5sv2", "text": "Good afternoon, everybody.", "is_final": true} ``` ```json Example with diarization theme={null} {"type": "transcript", "provider": "a5sv2", "text": "Shall we start? Sure, go ahead.", "is_final": false, "utterances": [ {"speaker": "speaker_0", "start": 0.4, "end": 1.36, "text": "Shall we start?", "is_final": true, "words": [{"text": "Shall", "start": 0.4, "end": 0.64, "speaker": "speaker_0"}, "..."]}, {"speaker": "speaker_1", "start": 1.92, "end": 2.88, "text": "Sure, go ahead.", "is_final": false, "words": ["..."]}], "speakers": ["speaker_0"]} ``` ### speaker\_identification Only with `speaker_identification`. One decision for a stretch of one speaker's speech, usually sent about a second after it ends, named or not. **speaker** `string` The speaker label, as in utterances. **start** `number` Start of the stretch, in seconds from the start of the session. **end** `number` End of the stretch, in seconds from the start of the session. **speaker\_id** `string | null` The enrolled speaker it was named as, or `null`. **name** `string | null` That speaker's name, or `null`. **score** `number | null` The best match's similarity score, -1 to 1. **margin** `number | null` How far the best score is above the next-best; `null` with one enrolled speaker. **matches** `object[]` Up to 3 closest enrolled speakers, best first: `{speaker_id, name, score}`. **compared\_duration** `number` Seconds of speech compared. **reason** `string` `accepted`, `score` (best score not above `threshold`), `margin` (too close to the next-best), or `error` (this stretch could not be compared; the session goes on). ```json Example theme={null} {"type": "speaker_identification", "speaker": "speaker_0", "start": 12.48, "end": 13.29, "speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781, "margin": 0.512, "compared_duration": 4.81, "reason": "accepted", "matches": [{"speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781}, {"speaker_id": "spk_01k73a7m6n5b4v3c2x1z0l9k8j", "name": "Ben Ito", "score": 0.269}]} ``` ### session\_limit The session reached its session length (30 minutes unless your account has a longer one) or the account's usage limit. The final transcript and `finished` follow, then close code 1000. **message** `string` Why the session ended. ### finished **audio\_ms** `integer` Audio received. **billed\_ms** `integer` Audio counted toward usage. **used\_ms** `integer` Free usage used after this session. **quota\_ms** `integer` The account's free usage. ### provider\_error Streaming failed during the session. An `error` follows. **message** `string` What failed. ### error Sent before the server ends the session abnormally; the connection then closes with the matching close code. **code** `string` One of the codes below. **message** `string` Human-readable detail. | code | Close | Meaning | | - | - | - | | `invalid_start` | 4400 | The first message is missing, late or invalid (including `speaker_identification` without `diarization`) | | `unsupported_language` | 4400 | `language` is not English | | `unknown_provider` | 4400 | `providers` names an unknown engine | | `speaker_not_found`, `speaker_not_enrolled`, `no_enrolled_speakers` | 4400 | `speaker_identification.speaker_ids` names an unknown speaker or one with no enrollments, or the account has no enrolled speakers | | `audio_before_ready` | 4400 | Audio sent before `ready` | | `invalid_audio_frame` | 4400 | An empty or odd-length audio frame | | `invalid_message` | 4400 | A text message other than `ping` or `finish` | | `unauthorized` | 4401 | Missing or invalid API key | | `account_disabled` | 4403 | The account is disabled | | `model_access_denied` | 4403 | No access to A5S Small V2 Streaming, or to AirCaps Speaker ID-v1 for `speaker_identification` ([talk to sales](https://aircaps.ai/contact)) | | `origin_not_allowed` | 4403 | Browser origin not allowed | | `idle_timeout` | 4408 | No messages for 45 seconds | | `quota_unavailable` | 4409 | Usage limit used up, or too many live sessions ([talk to sales](https://aircaps.ai/contact)) | | `insufficient_balance` | 4409 | Free usage and credits are used up: add credits in the console | | `realtime_rate_exceeded` | 4409 | Audio sent faster than real time | | `frame_too_large` | 4409 | An audio frame over 64 KB | | `a5sv2_unavailable`, `upstream_unavailable` | 1013 | Temporarily unavailable, at start or during the session; open a new session with backoff | | `diarization_unavailable` | 1013 | Speaker labels are temporarily unavailable; retry, or start without `diarization` | | `speaker_identification_unavailable` | 1013 | Speaker names are temporarily unavailable; retry, or start without `speaker_identification` | | `internal_error` | 1011 | Server error | ### pong The reply to `ping`. ## Close codes | Code | Meaning | | - | - | | 1000 | Normal close after `finished` | | 1011 | Server error | | 1013 | Temporarily unavailable; open a new session with backoff (for example 1 s, 2 s, 4 s … up to 30 s) | | 4400 | Invalid message or frame | | 4401 | Authentication failed | | 4403 | Forbidden: account disabled, no access to the model, or browser origin not allowed | | 4408 | Idle timeout | | 4409 | Limit reached at start (usage, sessions), audio faster than real time, or a frame over 64 KB | # Get subtitles Source: https://aircaps.ai/docs/api-reference/subtitles GET /v1/transcripts/{id}/subtitles SRT or WebVTT captions for a completed transcript, with the speaker in each cue. # Transcribe (one request) Source: https://aircaps.ai/docs/api-reference/transcribe POST /v1/transcribe Send the audio and get the finished transcript back in one request: the upload, [Create a transcript](/api-reference/create-transcript) and the wait in one call. The body is the raw audio (any audio or video format) with the options as query parameters, or a `multipart/form-data` form whose `file` field is the audio and whose other fields are the options. Options are those of Create a transcript except `audio_url` and `file_id`; `keyterms` may repeat, `metadata` and `speaker_identification` are JSON. If the transcript is not finished within `wait`, the answer is `202` with the transcript so far: fetch it with [Get a transcript](/api-reference/get-transcript), optionally with `wait`. # Update a speaker Source: https://aircaps.ai/docs/api-reference/update-speaker PATCH /v1/speakers/{id} Change the name or metadata. Omitted fields stay as they are; `null` clears one. # Upload a file Source: https://aircaps.ai/docs/api-reference/upload-file POST /v1/files Upload up to 1 GB in the request body. For larger files (up to 5 GB), use an upload URL. An upload that takes longer than about 2.5 minutes is answered with a 303 redirect to its result: follow redirects (most HTTP clients do by default). # Create an upload URL Source: https://aircaps.ai/docs/api-reference/upload-url POST /v1/files/upload-url Get a presigned URL to upload a file of up to 5 GB straight to storage. `PUT` the raw bytes to `upload_url` within an hour, then use `id` as `file_id`. # Get usage Source: https://aircaps.ai/docs/api-reference/usage GET /v1/usage Daily usage of each model your account can use. # API keys Source: https://aircaps.ai/docs/authentication Create, use and revoke keys. Every request needs an API key in the `Authorization` header: ```http theme={null} Authorization: Bearer aircaps_sk_... ``` * **Create keys** in the console: [console.aircaps.ai → API keys](https://console.aircaps.ai/api-keys). A key is shown once; AirCaps stores only a hash. * **One key, every model.** The same key works for the REST API and the streaming WebSocket. * **Up to 10 active keys** per account. Use one per service or environment so you can revoke them independently. * **Revoke** a key in the console. It stops working within 60 seconds; a streaming session already open with it runs until it ends. * **Keep keys server-side.** Never put a key in a browser, mobile app or public repository. Requests without a valid key return `401 unauthorized`; a disabled account returns `403 account_disabled`. On the streaming WebSocket these arrive as an `error` message with the same `code`. > [!NOTE] > > Account settings (keys, webhook signing secret, profile) can only be changed from the console, not with an API key. # Data & privacy Source: https://aircaps.ai/docs/data-privacy What AirCaps keeps, for how long, and how to delete it. **We don't use your audio or transcripts to train or improve models.** Your data is used only to process your requests. ## What we keep | Data | Kept for | | - | - | | **Streaming audio and transcripts** (A5S Small V2 Streaming) | Not stored. Audio is processed in memory and the text is sent only to you. | | **Uploaded files** (`POST /v1/files` or an upload URL) | Kept for 24 hours after upload, so you can reuse the `file_id`, then deleted within the following 24 hours (24 to 48 hours after upload). Deleting a transcript deletes its uploaded file right away. | | **Audio from `audio_url`** | Kept only as the processing copy (next row). The URL itself is kept with the transcript (below). | | **Processing copies** of the audio | Kept for 48 hours after the transcript is created, then deleted within the following 24 hours (48 to 72 hours). Deleting the transcript deletes them right away. | | **Transcripts** (text, words, speakers) | 30 days after completion, or until you delete them with [`DELETE /v1/transcripts/{id}`](/docs/api-reference/delete-transcript). | | **Transcript details you send** (`metadata`, `audio_url`, webhook URL) | Removed when the transcript is deleted or expires. | | **Speaker ID clips** (AirCaps Speaker ID-v1 enrollments and identifications) | Not kept. Each clip is used only to compute its speaker embedding and is discarded when the request finishes; clips are never saved to storage or to our database. | | **Speaker profiles** (AirCaps Speaker ID-v1: each speaker's name, metadata and the speaker embeddings derived from its enrollment clips) | Until you delete the speaker or the enrollment. There is no automatic expiry: your application decides who stays enrolled. Embeddings are numbers derived from a voice, not audio, and they can't be played back, but they identify a person's voice: treat them as biometric data. | | **Speaker identifications** (results) | Not stored. Only usage facts are kept (below). | | **Webhook auth header** (`webhook_auth_header_value`) | Removed as soon as the webhook is delivered or delivery fails for good. | | **Usage records** (duration, status, timestamps) | Kept for usage reporting and billing; they contain no audio, text or metadata. | | **Account** (email, name, limits) | For the life of the account. API keys are stored only as SHA-256 hashes. | | **Logs** | Requests (endpoint, status, timing and IDs). Logs don't contain audio or transcripts. | ## Deleting data * **A transcript:** `DELETE /v1/transcripts/{id}` deletes it immediately. If it is still processing, it is canceled. * **A speaker's voice data:** [`DELETE /v1/speakers/{id}`](/docs/api-reference/delete-speaker) deletes the speaker and all its embeddings immediately; [`DELETE /v1/speakers/{id}/enrollments/{enrollment_id}`](/docs/api-reference/delete-enrollment) deletes one enrollment. Both work even if your account no longer has access to the model. * **Uploads:** deleted automatically 24 to 48 hours after upload, or right away when you delete a transcript that used them. * **Processing copies:** deleted automatically 48 to 72 hours after the transcript is created, or right away when you delete it. * **Your account and all its data:** [contact us](https://aircaps.ai/contact). ## Voice data and consent AirCaps Speaker ID-v1 works by creating and storing a numerical representation of a person's voice. Laws such as the GDPR, the Illinois Biometric Information Privacy Act (BIPA) and other state privacy laws can treat this as biometric data. You are responsible for telling the people you enroll and identify, getting their consent where required, and deleting their speaker profiles when you no longer need them or when they ask. Delete with the endpoints above; deletion is immediate and permanent. ## Security * **In transit:** every connection uses TLS: your client to the API, between our services, to storage and to our database (with certificate verification). * **At rest:** stored data is encrypted with AES-256. * **Isolation:** every request is scoped to your account; no account can read another's data. * **Keys and webhooks:** API keys are stored as hashes and can be revoked at any time; webhooks are signed ([Standard Webhooks](/docs/guides/webhooks)). ## Where data is processed In the United States. For a data processing agreement, custom retention or a dedicated deployment, [talk to sales](https://aircaps.ai/contact). # Errors Source: https://aircaps.ai/docs/errors HTTP errors, job errors and retries. ## HTTP errors Every error uses one shape: ```json theme={null} { "error": { "type": "invalid_request_error", "code": "unsupported_language", "message": "language 'de' is not supported; only English (\"en\", \"en-US\", ...) is available today. Multilingual support is planned.", "param": "language_code", "request_id": "req_01k6z..." } } ``` | HTTP | type | code | | - | - | - | | 400 | `invalid_request_error` | `invalid_request`, `unsupported_language`, `unsupported_model`; AirCaps Speaker ID-v1 (and `speaker_identification` on transcripts): `invalid_audio`, `audio_too_short`, `audio_too_long`, `no_enrolled_speakers`, `speaker_not_enrolled` | | 401 | `authentication_error` | `unauthorized` | | 402 | `invalid_request_error` | `insufficient_balance` | | 403 | `permission_error` | `account_suspended`, `account_disabled`, `session_required`, `usage_limit_reached`, `model_access_denied` | | 404 | `invalid_request_error` | `not_found`, `file_not_found`, `speaker_not_found`, `enrollment_not_found` | | 405 | `invalid_request_error` | `method_not_allowed` | | 409 | `invalid_request_error` | `idempotency_conflict`, `transcript_not_ready`, `diarization_not_ready`, `speaker_limit_reached`, `enrollment_limit_reached` | | 413 | `invalid_request_error` | `file_too_large` | | 429 | `rate_limit_error` | `rate_limited`, `queue_full` (honour `Retry-After`) | | 500 | `api_error` | `internal_error` | | 503 | `api_error` | `service_unavailable`: a brief capacity or dependency problem (honour `Retry-After`) | | 502, 504 | `api_error` | `upstream_unavailable`, `upstream_timeout` (`request_id` is `null`) | Include `request_id` (also in the `x-request-id` header) when you contact support. * `unsupported_language`: only English is available today; multilingual support is planned. * `insufficient_balance`: your free hours and credits are used up. Add credits in the console (or turn on auto refill) to keep going. * `usage_limit_reached`, `model_access_denied`, `queue_full`: account limits. To raise them, [talk to sales](https://aircaps.ai/contact). * AirCaps Speaker ID-v1 reports problems with a clip in the response itself (it is synchronous): `invalid_audio` (not decodable or no audio), `audio_too_short` (under 0.5 s; enrollments under 1 s), `audio_too_long` (over 2 minutes), `file_too_large` (over 100 MB). These requests are not counted toward your usage. ## Job errors A transcript that fails has `"status": "error"` and an `error` object. None of these count toward your usage. | code | Meaning | | - | - | | `invalid_audio` | Not decodable, no audio stream, empty, or shorter than 0.1 s | | `audio_too_long` | Longer than 10 hours | | `file_too_large` | Larger than 5 GB | | `download_failed` | `audio_url` could not be fetched (HTTP error, timeout, non-public address) | | `file_not_found` | The uploaded file is missing or expired | | `insufficient_balance` | Costs more than your remaining free hours and credits. Add credits and resubmit | | `usage_limit_reached` | Longer than your remaining usage ([talk to sales](https://aircaps.ai/contact) to raise it) | | `internal_error` | Failed on our side after automatic retries. Resubmit; a `file_id` stays usable for 24 hours after upload | Deleting a queued or processing transcript cancels it: the `DELETE` response shows `error.code` `canceled`, no webhook is sent, and the ID then returns `404`. ## Retries * Retry `429`, `500`, `502`, `503` and `504` with exponential backoff (for example 1 s, 2 s, 4 s … up to 60 s), honouring `Retry-After` when present. * Send an `Idempotency-Key` header with `POST /v1/transcripts` so a retried request never creates a second job. A retry with the same key and body returns the original transcript (`200`), even after it has finished. * Live streaming: on close code 1013, a connection that can't be opened, one that drops without a close message, or one that goes quiet (no message for 30 seconds and no `pong` within 10 seconds of a `ping`), open a new session with backoff (for example 1 s, 2 s, 4 s … up to 30 s). See [Live streaming](/docs/guides/streaming#retrying). * Polling: call `GET /v1/transcripts/{id}` every 3–10 seconds, or use a [webhook](/docs/guides/webhooks) instead. * For large files, prefer an [upload URL](/docs/api-reference/upload-url): the bytes go straight to storage, so a retry never re-sends them through the API. # Batch transcription Source: https://aircaps.ai/docs/guides/async-transcription Transcribe recordings with A5S Async v1. ## 1. Provide the audio Choose one: | Source | How | Size | | - | - | - | | A URL | `audio_url` in the create request. Public or presigned http(s) URL. | ≤ 5 GB | | Upload URL | [`POST /v1/files/upload-url`](/docs/api-reference/upload-url), then `PUT` the bytes to the returned `upload_url` within an hour. Use the returned `id` as `file_id`. | ≤ 5 GB | | Direct upload | [`POST /v1/files`](/docs/api-reference/upload-file) with the raw bytes as the body. If it takes longer than about 2.5 minutes, the response is a `303` redirect to the result: follow redirects. | ≤ 1 GB | Any audio format ffmpeg can read works. The first audio stream is used; channels are mixed down. * **Uploaded files are kept for 24 hours after upload**, so you can use the same `file_id` for more than one transcript in that time, and are deleted within the following 24 hours (24 to 48 hours after upload). Deleting a transcript deletes its uploaded file right away. * An `audio_url` must be public or presigned, without credentials in the URL. Up to 5 redirects are followed (each must stay on a public address), and the download must finish within 15 minutes. ## 2. Create the transcript ```bash theme={null} curl https://api.aircaps.com/v1/transcripts \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: meeting-2026-10-06" \ -d '{ "model": "a5s-async-v1", "file_id": "file_01k6z...", "language_code": "en", "webhook_url": "https://example.com/hooks/aircaps", "metadata": {"meeting_id": 42} }' ``` `metadata` (up to 16 KB) is echoed back on the transcript and in webhooks. `language_code` is optional: `en` (the default) or a regional English code: `en-US`, `en-GB` (or `en-UK`), `en-AU`, `en-CA`, `en-IN`, `en-IE`, `en-NZ`, `en-ZA`, `en-SG`, `en-PH`, `en-HK`, `en-NG` or `en-KE`. For best results, use the one that matches the speakers' locale or accent. The transcript reports `language_code: en`. Multilingual support is planned. `timing_mode` sets the timestamp style for `words` and `utterances`: `phrase` (the default) or `region`. ### Key terms `keyterms` lists words and short phrases that may be spoken in the audio, such as people's names, company and product names, and jargon. The model is more likely to recognize them. ```json theme={null} "keyterms": ["AirCaps", "Dr. Patel", "GPT-4"] ``` * Up to 60 key terms, each at most 6 words and 50 characters. * The text stays verbatim and lowercase with numbers spelled out, so a key term appears as it is spoken: "GPT-4" as "gpt four". * List only terms that may come up. A key term can occasionally be written where a similar-sounding word was said. * The transcript echoes the list as `keyterms`. ### Large meetings For recordings with many active speakers, typically more than 7 to 9, such as large team meetings, panels, town halls and hearings, set `large_meeting` to `true`: ```json theme={null} "large_meeting": true ``` * It improves speaker attribution when many people take part. Processing may take a little longer. * Leave it off (the default) for typical meetings and conversations: the default is tuned for them. * The transcript echoes `large_meeting: true`. ## 3. Get the result Wait for a [webhook](/docs/guides/webhooks) or poll [`GET /v1/transcripts/{id}`](/docs/api-reference/get-transcript), adding `?wait=120` so each request returns as soon as the transcript finishes. To do it all in one request, send the audio to [`POST /v1/transcribe`](/docs/api-reference/transcribe): it uploads, creates the transcript and answers with the finished result (or `202` if it takes longer than `wait`, up to 120 s). | status | Meaning | | - | - | | `queued` | Waiting for one of your account's processing slots | | `processing` | Being transcribed | | `completed` | Done; result fields are present | | `error` | Failed; see `error` | ## The result ```json theme={null} { "id": "tr_01k6z3m4q8w7e5r2t1y0u9i8o7", "status": "completed", "model": "a5s-async-v1", "audio_duration": 3600.512, "text": "hey good afternoon everybody ...", "speakers": [{"id": "speaker_0", "speaking_time": 1402.11}, {"id": "speaker_1", "speaking_time": 988.4}], "utterances": [ {"speaker": "speaker_0", "start": 0.48, "end": 6.12, "text": "hey good afternoon everybody ...", "words": [{"text": "hey", "start": 0.48, "end": 0.71, "speaker": "speaker_0"}]} ], "words": [{"text": "hey", "start": 0.48, "end": 0.71, "speaker": "speaker_0"}] } ``` * Times are seconds from the start of the audio. * Speakers are numbered in order of first speech and are consistent within one transcript only. * Overlapping speech produces overlapping utterances. * Audio with no speech completes with empty `text`, `utterances` and `words`. Need captions? [`GET /v1/transcripts/{id}/subtitles?format=srt`](/docs/api-reference/subtitles). ## Speaker names With **AirCaps Speaker ID-v1** enabled on your account, a transcript can put names on its speakers. [Enroll the people](/docs/guides/speaker-id#1-create-a-speaker) you expect to hear, then add `speaker_identification` to the create request: ```bash theme={null} curl https://api.aircaps.com/v1/transcripts \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file_id": "file_01k6z...", "speaker_identification": { "threshold": 0.6, "speaker_ids": ["spk_01k73a9x8w7v6u5t4s3r2q1p0n", "spk_01k73a7m6n5b4v3c2x1z0l9k8j"] } }' ``` | Field | Default | | | - | - | - | | `threshold` | `0.6` | A speaker gets a name when an enrolled speaker is their best match and scores at least this (0 to 1). | | `speaker_ids` | every enrolled speaker | Compare with these people only, for example the meeting's invitees. | `"speaker_identification": true` uses both defaults. The completed transcript then has: ```json theme={null} { "speakers": [ {"id": "speaker_0", "speaking_time": 1402.11, "speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781, "compared_duration": 47.2, "matches": [{"speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781}, {"speaker_id": "spk_01k73a7m6n5b4v3c2x1z0l9k8j", "name": "Ben Ito", "score": 0.142}]}, {"id": "speaker_1", "speaking_time": 988.4, "speaker_id": null, "name": null, "score": null, "compared_duration": 41.3, "matches": [{"speaker_id": "spk_01k73a7m6n5b4v3c2x1z0l9k8j", "name": "Ben Ito", "score": 0.284}, "..."]} ], "utterances": [ {"speaker": "speaker_0", "speaker_name": "Ana García", "start": 0.48, "end": 6.12, "text": "hey good afternoon everybody ...", "words": ["..."]} ], "speaker_identification": { "model": "aircaps-speaker-id-v1", "status": "completed", "threshold": 0.6, "speaker_ids": ["spk_01k73a9x8w7v6u5t4s3r2q1p0n", "spk_01k73a7m6n5b4v3c2x1z0l9k8j"], "speakers_compared": 2, "speakers_identified": 1, "error": null } } ``` * `speaker` labels stay `speaker_0`, `speaker_1`, … in every field; names are added next to them. Subtitles show the names. * A speaker without a match has `speaker_id: null` and still lists its closest `matches`, so you can apply your own rule. * **Two speakers can get the same name**, for example when one person was split into two speakers. * A speaker with too little speech to compare isn't compared (`matches` is empty). * `speaker_identification.status` is `pending` until the transcript completes. If speaker identification is unavailable, the transcript still completes, without names, with `status: "failed"` and an `error`; create it again to retry. Speakers deleted before the transcript runs are skipped. * No extra usage: the transcript counts its audio duration, as always. ### Choosing a threshold A speaker gets a name when their best match among your enrolled speakers has a similarity score of at least `threshold` (0 to 1). The default, **0.6**, works best for most recordings. Adjust it to what works best for your audio: * **Raise it** when a wrong name is costly, or when many people who aren't enrolled may speak. * **Lower it** (for example to 0.5) when everyone who may speak is enrolled and you'd rather name more speakers. * Good enrollments matter as much as the threshold: follow [Enrollment audio: what works best](/docs/guides/speaker-id#enrollment-audio-what-works-best) and limit `speaker_ids` to the people who could be present. > [!NOTE] > > Without access to AirCaps Speaker ID-v1, requests with `speaker_identification` return `403 model_access_denied`. The named `speaker_ids` must exist and have at least one enrollment (`404 speaker_not_found`, `400 speaker_not_enrolled`), and without `speaker_ids` your account needs at least one enrolled speaker (`400 no_enrolled_speakers`). Voice data rules apply: see [Privacy and consent](/docs/guides/speaker-id#privacy-and-consent). ## Delete [`DELETE /v1/transcripts/{id}`](/docs/api-reference/delete-transcript) cancels a running job or deletes a finished transcript and its stored result. Transcripts are otherwise deleted after 30 days. # Speaker diarization Source: https://aircaps.ai/docs/guides/diarization Find who spoke when in a recording with AirCaps Diarization-v1. AirCaps Diarization-v1 answers **who spoke when**: it labels every stretch of speech in a recording with a speaker, including overlapping speech, and returns no transcript. Use it to segment meetings, interviews and calls by speaker, to count speakers, or to attach speakers to a transcript you already have. > [!NOTE] > > AirCaps Diarization-v1 is enabled per account. If `GET /v1/account` shows `"enabled": false` for `aircaps-diarization-v1`, requests return `403 model_access_denied`. To get access, [talk to sales](https://aircaps.ai/contact). ## 1. Provide the audio The same sources as [batch transcription](/docs/guides/async-transcription#1-provide-the-audio): an `audio_url`, or a `file_id` from an [upload URL](/docs/api-reference/upload-url) or a [direct upload](/docs/api-reference/upload-file). Any audio format ffmpeg can read works; channels are mixed down. Files are 0.1 s to 6 hours long. ## 2. Create the diarization ```bash theme={null} curl https://api.aircaps.com/v1/diarizations \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: meeting-2026-10-08" \ -d '{ "model": "aircaps-diarization-v1", "audio_url": "https://example.com/meeting.mp3", "webhook_url": "https://example.com/hooks/aircaps", "metadata": {"meeting_id": 42} }' ``` The response is a [diarization](/docs/api-reference/objects/diarization) with `status: "queued"` or `"processing"`. `model` is optional. `metadata` (up to 16 KB) is echoed back on the diarization and in webhooks. > [!TIP] > > There is no `language_code`: diarization works from the voices, not the words. ## 3. Get the result Wait for a [webhook](/docs/guides/webhooks) (`diarization.completed` or `diarization.failed`) or poll [`GET /v1/diarizations/{id}`](/docs/api-reference/get-diarization) every few seconds. | status | Meaning | | - | - | | `queued` | Waiting for one of your account's processing slots | | `processing` | Being diarized | | `completed` | Done; result fields are present | | `error` | Failed; see `error` | ## The result ```json theme={null} { "id": "dia_01k72p0x9h6t3b8r1n4m7q2w5e", "object": "diarization", "status": "completed", "model": "aircaps-diarization-v1", "audio_duration": 1834.112, "num_speakers": 3, "speakers": [ {"id": "speaker_0", "speaking_time": 702.31}, {"id": "speaker_1", "speaking_time": 611.08}, {"id": "speaker_2", "speaking_time": 240.6} ], "segments": [ {"speaker": "speaker_0", "start": 0.48, "end": 6.12}, {"speaker": "speaker_1", "start": 5.9, "end": 9.33}, {"speaker": "speaker_0", "start": 9.71, "end": 14.02} ], "speech_duration": 1498.27, "overlap_duration": 61.44 } ``` * `segments` are in time order. **Overlapping speech appears as overlapping segments of different speakers** (above, speaker 0 and speaker 1 both talk from 5.9 s to 6.12 s). * Times are seconds from the start of the audio, with millisecond precision. * Speakers are numbered in order of first speech and are consistent within one diarization only. The model finds the number of speakers itself. * `speech_duration` is the time anyone speaks; `overlap_duration` is the time two or more people speak at once. * Audio with no speech completes with `num_speakers: 0` and empty `speakers` and `segments`. ### RTTM [`GET /v1/diarizations/{id}/rttm`](/docs/api-reference/diarization-rttm) returns the same segments as an RTTM file, the format diarization scoring tools read: ```text theme={null} SPEAKER dia_01k72p0x9h6t3b8r1n4m7q2w5e 1 0.480 5.640 speaker_0 SPEAKER dia_01k72p0x9h6t3b8r1n4m7q2w5e 1 5.900 3.430 speaker_1 ``` ## Python example ```python theme={null} import os, time, requests API = "https://api.aircaps.com/v1" H = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} with open("meeting.wav", "rb") as f: file_id = requests.post(f"{API}/files", headers=H, data=f).json()["id"] job = requests.post(f"{API}/diarizations", headers=H, json={"model": "aircaps-diarization-v1", "file_id": file_id}).json() while job["status"] in ("queued", "processing"): time.sleep(5) job = requests.get(f"{API}/diarizations/{job['id']}", headers=H).json() for s in job.get("segments", []): print(f"{s['start']:8.2f} {s['end']:8.2f} {s['speaker']}") ``` ## Limits and usage * Audio is billed by duration and counts toward your account's [usage limit](/docs/limits), shared by all models. Failed jobs don't count. * Diarizations share your account's asynchronous job limits (concurrent jobs, queue, requests per minute) with transcripts. * Files up to 6 hours. Longer files fail with `audio_too_long`. ## Delete [`DELETE /v1/diarizations/{id}`](/docs/api-reference/delete-diarization) cancels a running job or deletes a finished diarization, its stored result and the uploaded file it used. Diarizations are otherwise deleted after 30 days. # Code samples Source: https://aircaps.ai/docs/guides/samples Copy-paste examples in Python, Node.js and cURL. ## Transcribe a local file ```python Python theme={null} # pip install requests import os, sys, time, requests API = "https://api.aircaps.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} def call(method, path, **kw): r = requests.request(method, API + path, headers=HEADERS, timeout=60, **kw) if r.status_code >= 400: raise SystemExit(r.json()["error"]["message"]) return r.json() upload = call("POST", "/files/upload-url") with open(sys.argv[1], "rb") as f: requests.put(upload["upload_url"], data=f, timeout=600).raise_for_status() job = call("POST", "/transcripts", json={"model": "a5s-async-v1", "file_id": upload["id"], "language_code": "en"}) while job["status"] in ("queued", "processing"): time.sleep(3) job = call("GET", f"/transcripts/{job['id']}") if job["status"] == "error": raise SystemExit(job["error"]["message"]) for u in job["utterances"]: print(f"[{u['start']:8.1f}s] {u['speaker']}: {u['text']}") ``` ```javascript Node.js theme={null} // node transcribe.mjs meeting.mp3 (Node 18+) import { readFile } from "node:fs/promises"; const API = "https://api.aircaps.com/v1"; const headers = { Authorization: `Bearer ${process.env.AIRCAPS_API_KEY}` }; async function call(method, path, body) { const r = await fetch(API + path, { method, headers: body ? { ...headers, "Content-Type": "application/json" } : headers, body: body ? JSON.stringify(body) : undefined, }); const data = await r.json(); if (!r.ok) throw new Error(data.error.message); return data; } const upload = await call("POST", "/files/upload-url"); const put = await fetch(upload.upload_url, { method: "PUT", body: await readFile(process.argv[2]) }); if (!put.ok) throw new Error(`upload failed: ${put.status}`); let job = await call("POST", "/transcripts", { model: "a5s-async-v1", file_id: upload.id, language_code: "en" }); while (job.status === "queued" || job.status === "processing") { await new Promise((r) => setTimeout(r, 3000)); job = await call("GET", `/transcripts/${job.id}`); } if (job.status === "error") throw new Error(job.error.message); for (const u of job.utterances) console.log(`[${u.start.toFixed(1)}s] ${u.speaker}: ${u.text}`); ``` ```bash cURL theme={null} # 1. upload URL curl -X POST https://api.aircaps.com/v1/files/upload-url -H "Authorization: Bearer $AIRCAPS_API_KEY" # 2. upload the bytes to the returned upload_url curl -X PUT --upload-file meeting.mp3 "" # 3. transcribe with the returned id curl https://api.aircaps.com/v1/transcripts -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" -d '{"model": "a5s-async-v1", "file_id": "file_...", "language_code": "en"}' ``` ## Stream a WAV file ```python theme={null} # pip install websockets==15.0.1 # AIRCAPS_API_KEY=aircaps_sk_... python stream.py audio.wav (mono 16 kHz PCM16 WAV) import asyncio, json, os, sys, time, wave import websockets async def main(): headers = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} async with websockets.connect("wss://api.aircaps.com/v1/realtime", additional_headers=headers) as ws: await ws.send(json.dumps({"type": "start", "providers": ["a5sv2"], "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1, "language": "en"})) while (event := json.loads(await ws.recv()))["type"] != "ready": if event["type"] == "error": raise RuntimeError(event["message"]) async def receive(): final = "" async for message in ws: event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: # text is the whole transcript so far: print only the newly settled part text = event["text"] print((text[len(final):] if text.startswith(final) else text).strip(), flush=True) final = text elif event["type"] in ("session_limit", "error"): print(event["message"], flush=True) receiver = asyncio.create_task(receive()) try: with wave.open(sys.argv[1], "rb") as audio: start, sent = time.monotonic(), 0 while chunk := audio.readframes(320): # 20 ms await ws.send(chunk) sent += len(chunk) // 2 await asyncio.sleep(max(0, start + sent / 16000 - time.monotonic())) await ws.send(json.dumps({"type": "finish"})) except websockets.ConnectionClosedOK: pass # the server ended the session (session length) await receiver asyncio.run(main()) ``` Convert any file to the right WAV format with `ffmpeg -i input.mp3 -ac 1 -ar 16000 -sample_fmt s16 audio.wav`. ## Verify a webhook ```python theme={null} # pip install flask standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook, WebhookVerificationError app = Flask(__name__) wh = Webhook(os.environ["AIRCAPS_WEBHOOK_SECRET"]) @app.post("/hooks/aircaps") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except WebhookVerificationError: return "", 400 print(event["event"], event["id"]) # fetch the transcript with GET /v1/transcripts/{id} return "", 204 ``` # Speaker identification Source: https://aircaps.ai/docs/guides/speaker-id Recognize enrolled people by voice with AirCaps Speaker ID-v1: enroll, then score clips. AirCaps Speaker ID-v1 tells you **which of your enrolled people a clip sounds like**. You enroll each person with one or more voice clips, then send a clip and get back a similarity score for every enrolled speaker, best first. Use it to put names on diarized speakers, check that the expected person is speaking, or route audio by speaker. It is synchronous: one clip per request, and the scores are the response. There are no jobs to poll and no webhooks. > [!TIP] > > **Want names in a transcript?** Add `speaker_identification` when you [create a transcript](/docs/guides/async-transcription#speaker-names), or to the `start` message of a [live stream](/docs/guides/streaming#speaker-names) with speaker labels: each speaker gets the name of the enrolled speaker they match when the score reaches your threshold, with no cropping or clips to send. > [!NOTE] > > AirCaps Speaker ID-v1 is enabled per account. If `GET /v1/account` shows `"enabled": false` for `aircaps-speaker-id-v1`, requests return `403 model_access_denied`. To get access, [talk to sales](https://aircaps.ai/contact). ## How it works 1. A **speaker** is a name plus optional metadata that you create. 2. Each **enrollment** is one clip of that speaker. The model turns it into a voice embedding, which is stored; the audio is not. A speaker's **profile** combines all of its enrollments. 3. **Identify** compares a clip with every profile and returns a similarity `score` for each speaker, from -1 to 1. Higher means more alike. 4. **You decide.** There is no threshold and no "unknown" verdict: your application chooses what score to accept. [Scores and thresholds](#scores-and-thresholds) gives measured starting points. > [!WARNING] > > **The whole clip is compared, exactly as you send it.** Silence, music, background voices and crosstalk all become part of the voice the model hears. Before you send a clip, crop it to **one person speaking**: for example, use the segments from [AirCaps Diarization-v1](/docs/guides/diarization) or the utterance times of a [transcript](/docs/guides/async-transcription). Cropping, choosing which clips to send and combining results over time are up to your application. (For recordings you transcribe with A5S Async v1, or live streams with A5S Small V2 Streaming, `speaker_identification` does all of this for you: see [transcripts](/docs/guides/async-transcription#speaker-names) and [live streams](/docs/guides/streaming#speaker-names).) ## 1. Create a speaker ```bash theme={null} curl https://api.aircaps.com/v1/speakers \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Ana García", "metadata": {"employee_id": "E-1042"}}' ``` The response is a [speaker](/docs/api-reference/objects/enrolled-speaker) with an `id` (`spk_…`) and no enrollments yet. `name` and `metadata` (any JSON object up to 16 KB) are optional; use pseudonymous labels if you prefer. ## 2. Enroll clips Send each clip's **raw audio bytes as the request body** (not multipart form data). Any common audio or video format works; channels are mixed down and audio is resampled to 16 kHz. ```bash theme={null} curl https://api.aircaps.com/v1/speakers/spk_01k73a9x8w7v6u5t4s3r2q1p0n/enrollments \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: audio/wav" \ --data-binary @ana_intro.wav ``` ```json theme={null} { "id": "enr_01k73b5f1h2j3k4m5n6p7q8r9s", "object": "speaker_enrollment", "speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "audio_duration": 15.42, "consistency": 0.712, "created_at": "2026-10-08T18:04:52.015Z", "speaker": { "id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "num_enrollments": 2, "enrolled_duration": 27.9, "...": "..." } } ``` `consistency` shows how well this enrollment matches the speaker's other enrollments (`null` for the first one). If one enrollment scores much lower than the others, it may be someone else's voice or a poor recording: [delete it](/docs/api-reference/delete-enrollment). ### Enrollment audio: what works best | Recommendation | Why | | - | - | | **20–30 seconds of the person's speech in total**, for example 2–3 clips of about 10 s each | More enrollment speech raises the weakest same-person scores. Gains level off after about 20–30 s | | **Several clips rather than one**, ideally from different recordings | For the same total, several clips do at least as well as one long clip, and clips from different sessions cover more of how the person sounds | | **One person speaking**, with no music and little background noise | Everything in the clip is treated as the person's voice | | **Natural speech** (a conversation, reading a paragraph), not single words | Each clip must be at least 1 s; clips of several seconds represent a voice much better than very short ones | | **The same kind of microphone and room you will identify from** | A profile enrolled on a headset and compared with a far-away room microphone typically scores lower | | **Lossless or high-bitrate audio** (WAV, FLAC, Opus or AAC at normal bitrates) | Very low-bitrate audio (such as 24 kbps MP3) lowers scores noticeably | A single clean 5–10 s clip is enough to get started. A speaker can have up to 20 enrollments. ## 3. Identify a clip ```bash theme={null} curl https://api.aircaps.com/v1/speakers/identify \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: audio/wav" \ --data-binary @clip.wav ``` ```json theme={null} { "id": "sid_01k73c0q9w8e7r6t5y4u3i2o1p", "object": "speaker_identification", "model": "aircaps-speaker-id-v1", "created_at": "2026-10-08T18:06:30.551Z", "audio_duration": 4.21, "speakers_compared": 3, "margin": 0.476, "results": [ {"speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.684, "enrollment_scores": [{"enrollment_id": "enr_01k73b2c4d5e6f7g8h9j0k1m2n", "score": 0.661}, {"enrollment_id": "enr_01k73b5f1h2j3k4m5n6p7q8r9s", "score": 0.648}]}, {"speaker_id": "spk_01k73a7m6n5b4v3c2x1z0l9k8j", "name": "Ben Ito", "score": 0.208, "enrollment_scores": [{"enrollment_id": "enr_01k73a8h7g6f5d4s3a2p1o0i9u", "score": 0.208}]}, {"speaker_id": "spk_01k73a5q4w3e2r1t0y9u8i7o6p", "name": null, "score": 0.031, "enrollment_scores": [{"enrollment_id": "enr_01k73a6z5x4c3v2b1n0m9l8k7j", "score": 0.031}]} ] } ``` * `results` lists every compared speaker, best first. `score` compares the clip with the speaker's profile (all of their enrollments together); `enrollment_scores` compares it with each enrollment on its own. * `margin` is the best score minus the second best (`null` when only one speaker was compared). * By default the clip is compared with **every enrolled speaker** in your account. Limit it with `speaker_ids` (comma-separated, or the parameter repeated), for example the people invited to a meeting. Return fewer results with `top_k`. * **To verify one person**, pass just their ID: `?speaker_ids=spk_01k73a9x8w7v6u5t4s3r2q1p0n`, and compare the `score` with your threshold. * Clips are 0.5 s to 2 minutes long. Identification results are not stored. ## Scores and thresholds A score is a similarity, not a probability. What counts as "the same person" depends on your audio, so treat the numbers below as a starting point and check them on your own recordings. We measured them on meeting recordings from a single far-field microphone. Each person was enrolled from one meeting with about 15 s of speech and identified in other meetings among the people present. Clips were cropped to one person's speech. | Clip length | Correct top match | Same person: typical score (90% above) | Different person: typical score (90% below) | | -: | -: | -: | -: | | 1 s | 95% | 0.58 (0.34) | 0.11 (0.28) | | 2 s | 97.5% | 0.68 (0.50) | 0.11 (0.29) | | 3 s | 99% | 0.73 (0.58) | 0.11 (0.29) | | 5 s | 99.7% | 0.77 (0.64) | 0.10 (0.30) | | 10 s | 100% | 0.82 (0.68) | 0.11 (0.29) | **Longer clips are the biggest lever.** Clips of 3 s or more are much more reliable than 1 s clips; if your segments are short, combine several from the same speaker before identifying. How common decision rules behaved in the same test: | Accept the top speaker when… | 2 s clips: right / wrong / no answer | 5 s clips: right / wrong / no answer | Person not enrolled, wrongly named (2 s / 5 s) | | - | - | - | - | | `score ≥ 0.4` | 95% / 1.1% / 3% | 99% / 0.3% / 0% | 6.1% / 5.0% | | `score ≥ 0.5` | 91% / 0.5% / 9% | 99% / 0.3% / 1% | 1.1% / 2.2% | | `score ≥ 0.5` and `margin ≥ 0.2` | 86% / 0.2% / 14% | 96% / 0% / 4% | 0.9% / 1.7% | | `score ≥ 0.6` and `margin ≥ 0.2` | 75% / 0.2% / 25% | 93% / 0% / 7% | 0.2% / 0% | * **A good default:** accept the top speaker when `score ≥ 0.5` and `margin ≥ 0.2`; otherwise treat the clip as unknown. * **If everyone who might speak is enrolled** (a closed group), a lower score such as 0.4 names more clips with few mistakes. * **If unenrolled people may speak**, or a wrong name is costly, require a higher score (0.6) and a margin. The last column is the rate at which someone who isn't enrolled gets named as somebody else. * Enrolling more speech per person (20–30 s) and comparing only against the people who could be present (`speaker_ids`) both reduce mistakes. ## Python example ```python theme={null} import os, requests API = "https://api.aircaps.com/v1" H = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} def post_clip(path, wav_path, **params): with open(wav_path, "rb") as f: r = requests.post(f"{API}{path}", headers={**H, "Content-Type": "audio/wav"}, data=f, params=params) r.raise_for_status() return r.json() # enroll ana = requests.post(f"{API}/speakers", headers=H, json={"name": "Ana García"}).json() for clip in ("ana_1.wav", "ana_2.wav", "ana_3.wav"): post_clip(f"/speakers/{ana['id']}/enrollments", clip) # identify, then apply your own rule res = post_clip("/speakers/identify", "segment_0042.wav") top = res["results"][0] if top["score"] >= 0.5 and (res["margin"] is None or res["margin"] >= 0.2): print("speaker:", top["name"], top["score"]) else: print("unknown speaker; best guess", top["name"], top["score"]) ``` To cut a clip out of a longer recording first, for example the segment from 12.3 s to 19.8 s: ```bash theme={null} ffmpeg -ss 12.3 -to 19.8 -i meeting.wav -ac 1 -ar 16000 segment_0042.wav ``` ## Managing speakers * [List](/docs/api-reference/list-speakers) and [get](/docs/api-reference/get-speaker) speakers with their enrollments; [rename or change metadata](/docs/api-reference/update-speaker). * [Delete an enrollment](/docs/api-reference/delete-enrollment) to drop a bad clip; the profile is recomputed from the rest. * [Delete a speaker](/docs/api-reference/delete-speaker) to remove the person and all their voice data, immediately and permanently. * Speakers are kept until you delete them; there is no automatic expiry. You can always read and delete them, even if your account loses access to the model. ## Limits and usage * Each clip that is processed (enrollment or identification) counts its duration toward your [usage limit](/docs/limits), shared by all models. Rejected clips don't count. * Clips: 0.5 s to 2 minutes (enrollments at least 1 s), up to 100 MB, one per request. * 1,000 speakers per account, 20 enrollments per speaker, 600 requests per minute (enrollments and identifications together). To raise them, [talk to sales](https://aircaps.ai/contact). * Errors: `invalid_audio`, `audio_too_short`, `audio_too_long`, `file_too_large`, `no_enrolled_speakers`, `speaker_not_enrolled`, `speaker_not_found`. See [Errors](/docs/errors). ## Privacy and consent Clips are never stored. Speaker profiles (names, metadata and voice embeddings) are kept until you delete them. Voice embeddings can count as biometric data under laws such as the GDPR and Illinois BIPA, so tell the people you enroll and identify, get their consent where required, and delete their profiles when you no longer need them. See [Data & privacy](/docs/data-privacy#voice-data-and-consent). # Live streaming Source: https://aircaps.ai/docs/guides/streaming Real-time captions with A5S Small V2 Streaming over WebSocket. Endpoint: `wss://api.aircaps.com/v1/realtime` ## Protocol **1. Connect** Open the WebSocket with your key in the handshake header: `Authorization: Bearer aircaps_sk_...` **2. Send start** The first message is JSON. `language` accepts only `en`; multilingual support is planned. ```json theme={null} { "type": "start", "providers": ["a5sv2"], "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1, "language": "en" } ``` **3. Wait for ready** The server may send `provider_status` messages while the session starts, then `ready`. Do not send audio before `ready`. **4. Stream audio** Send binary frames of mono 16 kHz signed 16-bit little-endian PCM, in real time. 20 ms frames (640 bytes) are recommended; frames may be at most 64 KB. Send `{"type": "ping"}` to keep the connection open while you have no audio to send. **5. Finish** Send `{"type": "finish"}`. The server sends the final transcript, then a `finished` message, and closes. ## Messages from the server ```json theme={null} {"type": "transcript", "provider": "a5sv2", "text": "The complete transcript so far.", "is_final": false} ``` `text` is always the complete transcript so far; replace what you display rather than appending. For long sessions, set `"transcript_text": "incremental"` in `start` so messages stay small. Each `transcript` then carries an edit instead of `text`: remove `text_delete` characters from the end of the text you have, then append `text_append`. The result is always the complete transcript so far. ```json theme={null} {"type": "transcript", "provider": "a5sv2", "text_delete": 4, "text_append": "there, everyone", "is_final": false} ``` ```python theme={null} text = "" def on_transcript(message): global text text = text[:len(text) - message["text_delete"]] + message["text_append"] ``` | type | Meaning | | - | - | | `provider_status` | Startup progress (`message`) | | `ready` | Start sending audio. Includes `session_limit_ms` and `diarization` | | `transcript` | Current text; `is_final` is true when a segment is settled. With speaker labels, also `utterances` and `speakers` | | `session_limit` | The session length or your usage limit was reached; the final transcript and `finished` follow | | `provider_error` | Streaming failed during the session (`message`); an `error` follows | | `error` | `code` and `message`; the connection then closes | | `finished` | `audio_ms` streamed and `billed_ms` counted toward usage | | `pong` | Reply to `ping` | ## Key terms Key terms bias recognition toward names and jargon. Send up to 64 in `custom_vocabulary`, each with a `text` (up to 100 characters) and a `strength` from 0 to 12: ```json theme={null} { "type": "start", "providers": ["a5sv2"], "custom_vocabulary_enabled": true, "custom_vocabulary": [{"text": "AirCaps", "strength": 6}] } ``` ## Speaker labels Set `"diarization": true` in `start` to label who said what, live. Every `transcript` keeps the complete `text` and adds `utterances` and `speakers`: * Utterances with `is_final: true` became final since the previous message. Each is sent once and never changes: keep it. * Utterances with `is_final: false` are pending and may still change (their text, speaker or boundaries). Replace the previous pending ones each time. * Show the kept final utterances and the latest pending ones, ordered by `start`. Labels are `speaker_0`, `speaker_1`, … in order of first speech, as in async transcripts, and stay the same for the whole session. Words get their speaker about a second after they are spoken; until then, a pending utterance may show a provisional label, or `null` before anyone has been recognized. Overlapping speech comes out as interleaved utterances. ```python theme={null} final, pending = [], [] def on_transcript(message): global pending for utterance in message.get("utterances", []): if utterance["is_final"]: final.append(utterance) pending = [u for u in message.get("utterances", []) if not u["is_final"]] for u in sorted(final + pending, key=lambda u: u["start"]): print(u["speaker"], u["text"]) ``` If speaker labels are briefly unavailable, the session ends with `diarization_unavailable` (close code 1013); retry, or start without `diarization`. ## Speaker names With **AirCaps Speaker ID-v1** enabled on your account, a live session can also put names on its speakers. [Enroll the people](/docs/guides/speaker-id#1-create-a-speaker) you expect to hear, then add `speaker_identification` to `start`, together with `"diarization": true` (names need speaker labels): ```json theme={null} { "type": "start", "diarization": true, "speaker_identification": { "threshold": 0.6, "speaker_ids": ["spk_01k73a9x8w7v6u5t4s3r2q1p0n", "spk_01k73a7m6n5b4v3c2x1z0l9k8j"] } } ``` | Field | Default | | | - | - | - | | `threshold` | `0.6` | A speaker gets a name when their best match among your enrolled speakers scores above this (0 to 1) and is clearly ahead of the next-best. The default works best for most audio; adjust it to what works best for yours. | | `speaker_ids` | every enrolled speaker | Compare with these people only, for example the meeting's invitees. | `"speaker_identification": true` uses both defaults. `ready` confirms it with `"speaker_identification": {"model": "aircaps-speaker-id-v1", "threshold": 0.6, "speaker_ids": [...], "speakers_compared": 2}` (`speaker_ids` is `null` when comparing with every enrolled speaker). Names arrive as `speaker_identification` messages, one for each stretch of a speaker's speech, named or not: ```json theme={null} {"type": "speaker_identification", "speaker": "speaker_0", "start": 12.48, "end": 13.29, "speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781, "margin": 0.512, "compared_duration": 4.81, "reason": "accepted", "matches": [{"speaker_id": "spk_01k73a9x8w7v6u5t4s3r2q1p0n", "name": "Ana García", "score": 0.781}, {"speaker_id": "spk_01k73a7m6n5b4v3c2x1z0l9k8j", "name": "Ben Ito", "score": 0.269}]} ``` Utterances also carry `speaker_id` and `speaker_name`: the decision for the same `speaker` that overlaps them most, as known when the utterance is sent (`null` until then). Final utterances are never resent, so to name one after the fact, apply later `speaker_identification` messages the same way: ```python theme={null} decisions = [] def on_speaker_identification(message): decisions.append(message) def name_of(utterance): best, overlap = None, 0.0 for d in decisions: if d["speaker"] != utterance["speaker"]: continue o = min(d["end"], utterance["end"]) - max(d["start"], utterance["start"]) if o > overlap: best, overlap = d, o return (best and best["name"]) or utterance["speaker"] ``` * Labels stay `speaker_0`, `speaker_1`, …; names are added next to them. * An unnamed decision still lists its closest `matches` (up to 3), so you can apply your own rule. `reason` says why: `accepted`, `score` (best score not above `threshold`), `margin` (too close to the next-best), or `error` (this stretch could not be compared; the session goes on). * Two speakers can get the same name, for example when one person is briefly split into two speakers. * No extra usage: the session counts its audio duration, as always. Without access to AirCaps Speaker ID-v1, a `start` with `speaker_identification` ends with `model_access_denied` (4403); without `"diarization": true`, with `invalid_start` (4400). The named `speaker_ids` must exist and have at least one enrollment (`speaker_not_found`, `speaker_not_enrolled`), and without `speaker_ids` your account needs at least one enrolled speaker (`no_enrolled_speakers`); these close with 4400. If speaker names are briefly unavailable, the session ends with `speaker_identification_unavailable` (1013); retry, or start without `speaker_identification`. Voice data rules apply: see [Privacy and consent](/docs/guides/speaker-id#privacy-and-consent). ## Rules * Send the `start` message within 45 seconds of connecting. It accepts only the documented fields. * Audio must arrive in real time; sending faster (beyond a 2-second burst) ends the session with `realtime_rate_exceeded`. * A session can last up to 30 minutes (`session_limit`); open a new one to continue. Longer sessions, up to 4 hours, are available on request ([talk to sales](https://aircaps.ai/contact)). Your account's session length is `max_session_seconds` in [`GET /v1/account`](/docs/api-reference/account). * No messages (audio or `ping`) for 45 seconds ends the session with `idle_timeout`. * At most 5 sessions per account at once. ## Errors Every session that ends abnormally sends an `error` message with a `code`, then closes with the matching WebSocket close code. | code | Close code | Meaning | | - | - | - | | `invalid_start`, `unsupported_language`, `unknown_provider` | 4400 | Bad or missing `start` message | | `speaker_not_found`, `speaker_not_enrolled`, `no_enrolled_speakers` | 4400 | `speaker_identification` names an unknown or unenrolled speaker, or the account has no enrolled speakers | | `audio_before_ready`, `invalid_audio_frame`, `invalid_message` | 4400 | Audio sent before `ready`, an empty or odd-length audio frame, or a text message other than `ping`/`finish` | | `unauthorized` | 4401 | Missing or invalid API key | | `account_disabled`, `model_access_denied`, `origin_not_allowed` | 4403 | Account disabled, no access to A5S Small V2 Streaming (or to AirCaps Speaker ID-v1 for `speaker_identification`), or browser origin not allowed | | `idle_timeout` | 4408 | No messages for 45 seconds | | `quota_unavailable`, `realtime_rate_exceeded`, `frame_too_large` | 4409 | No usage left or too many live sessions; audio faster than real time; a frame over 64 KB ([talk to sales](https://aircaps.ai/contact) to raise limits) | | `a5sv2_unavailable`, `upstream_unavailable` | 1013 | Temporarily unavailable; retry with backoff | | `diarization_unavailable` | 1013 | Speaker labels are temporarily unavailable; retry, or start without `diarization` | | `speaker_identification_unavailable` | 1013 | Speaker names are temporarily unavailable; retry, or start without `speaker_identification` | | `internal_error` | 1011 | Server error | A normal session ends with `finished` and close code 1000. ### Retrying Close code 1013 means try again, whether the session was starting or already streaming: open a new session, waiting longer between attempts (for example 1 s, 2 s, 4 s … up to 30 s, with some jitter). Do the same if the connection can't be opened (for example a handshake timeout, or a refused or reset connection) or drops without a close message. A session that ends early counts only the audio you streamed. A connection can also go quiet without closing. To notice: if nothing arrives from the server for 30 seconds, send `{"type": "ping"}`; if no `pong` arrives within 10 seconds, close the connection and open a new session as above. See the [Python sample](/docs/guides/samples#stream-a-wav-file). # Webhooks Source: https://aircaps.ai/docs/guides/webhooks Get notified when a transcript or diarization finishes. Pass `webhook_url` when you create a transcript or a diarization. When it finishes, AirCaps sends a `POST`: ```json theme={null} {"event": "transcript.completed", "id": "tr_01k6z...", "status": "completed", "metadata": {"meeting_id": 42}} ``` `event` is `transcript.completed` or `transcript.failed` (for diarizations, `diarization.completed` or `diarization.failed`, with a `dia_...` ID). The payload never contains the result; fetch it with [`GET /v1/transcripts/{id}`](/docs/api-reference/get-transcript) or [`GET /v1/diarizations/{id}`](/docs/api-reference/get-diarization). * To add your own header to each delivery, set `webhook_auth_header_name` and `webhook_auth_header_value` on the request. * `webhook_url` must be a public http(s) address. Redirects are not followed. * Canceled jobs send no webhook. ## Verify signatures Deliveries are signed per [Standard Webhooks](https://www.standardwebhooks.com) with your account's webhook secret (`whsec_...`). Find and rotate it in the console under **API keys → Webhook signing secret**. Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature: v1,`. ```python theme={null} # pip install standardwebhooks import os from standardwebhooks.webhooks import Webhook, WebhookVerificationError wh = Webhook(os.environ["AIRCAPS_WEBHOOK_SECRET"]) try: event = wh.verify(request_body_bytes, request_headers) # raw body bytes and the request headers except WebhookVerificationError: ... # reject with 400 ``` Manual check: 1. Base64-decode the secret without its `whsec_` prefix. 2. Compute HMAC-SHA256 with it over `{webhook-id}.{webhook-timestamp}.{raw body}` and base64-encode the result. 3. Compare it in constant time with the value after `v1,` in `webhook-signature`. 4. Reject timestamps more than 5 minutes old. ## Delivery * Reply with any 2xx within 15 seconds. * Failures (including timeouts, network errors and 3xx) are retried for about 24 hours: after 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h and 8 h. * A 4xx other than 408 or 429 stops retries. * `webhook-id` is stable across retries of one event; use it to deduplicate. * The transcript shows `webhook_status` (`pending`, `delivered` or `failed`) and the last HTTP status as `webhook_status_code`. * Rotating the secret applies to pending retries too. # Speech-to-text, diarization and speaker ID API Source: https://aircaps.ai/docs/index Speech-to-text, speaker diarization and speaker identification: live streaming, batch transcription, who spoke when, and who is speaking. The AirCaps AI API has four models: | Model | ID | Use it for | | - | - | - | | **A5S Async v1** | `a5s-async-v1` | Recorded audio: transcripts with speaker labels, word timestamps, subtitles | | **A5S Small V2 Streaming** | `a5s-small-v2-streaming` | Live audio: captions as people speak | | **AirCaps Diarization-v1** | `aircaps-diarization-v1` | Recorded audio: who spoke when, without a transcript | | **AirCaps Speaker ID-v1** | `aircaps-speaker-id-v1` | Enrolled voices: which known person is speaking | Base URL: `https://api.aircaps.com/v1` **Every account includes 5 hours of free usage**, shared by all models. After that, usage is pay as you go: add credits in the console, where the prices are listed. AirCaps Diarization-v1 and AirCaps Speaker ID-v1 are enabled per account on request. For volume pricing, higher limits or concurrency, model access, or a dedicated deployment, [talk to sales](https://aircaps.ai/contact). We don't train on your data; see [Data & privacy](/docs/data-privacy). A5S Async v1 and A5S Small V2 Streaming transcribe **English** today. Multilingual support is planned and will be available soon. AirCaps Diarization-v1 and AirCaps Speaker ID-v1 work from the voice, not the words. > [card] [Quickstart](/docs/quickstart) > > A first request to each model. > [card] [API keys](/docs/authentication) > > Create and use a key. > [card] [Batch transcription](/docs/guides/async-transcription) > > Uploads, URLs, polling and webhooks. > [card] [Live streaming](/docs/guides/streaming) > > The realtime WebSocket protocol. > [card] [Speaker diarization](/docs/guides/diarization) > > Who spoke when in a recording. > [card] [Speaker identification](/docs/guides/speaker-id) > > Enroll voices, then recognize them. > [card] [Talk to sales](https://aircaps.ai/contact) > > Pricing, higher limits and concurrency, model access, and dedicated deployments. > [!TIP] > > Try every model without code in the [Playground](https://console.aircaps.ai/playground). # Limits Source: https://aircaps.ai/docs/limits Free usage, credits, concurrency and rate limits. ## Free usage and credits Each account includes **5 hours of free usage**, shared by all models. After that, usage is paid from credits you add in the console (Billing, where the prices are listed). Credits added early are used only once the free hours run out; they don't expire. * **A5S Async v1** counts the duration of each completed transcript. Failed jobs are not counted. * **AirCaps Diarization-v1** counts the duration of each completed diarization the same way. * **A5S Small V2 Streaming** counts the audio you stream. * **AirCaps Speaker ID-v1** counts the duration of each clip it processes, enrollments and identifications alike. Requests that fail are not counted. * When the free hours and credits are used up, new requests return `402 insufficient_balance`, a file that costs more than what's left fails with `insufficient_balance` (not charged), and a live stream ends with a `session_limit` message. Add credits, or turn on auto refill in the console, to keep going. * See what's left in the console or with [`GET /v1/account`](/docs/api-reference/account) (`usage_limit` is your free usage). ## Defaults | Limit | Default | | - | - | | Asynchronous jobs processing at once (transcripts and diarizations together) | 5 (more are queued, first in first out) | | Asynchronous jobs waiting in the queue | 2,000 | | Job create requests (`POST /v1/transcripts` and `POST /v1/diarizations` together) | 600 per minute | | Maximum file duration / size | 10 hours / 5 GB (AirCaps Diarization-v1: 6 hours / 5 GB) | | Direct upload (`POST /v1/files`) | 1 GB (use an upload URL for larger files). One that takes longer than about 2.5 minutes is answered with a `303` redirect to its result: follow redirects, as most HTTP clients do by default | | Uploaded files | Usable for 24 hours after upload, then deleted within the next 24 hours; deleting a transcript deletes its file right away | | Transcripts and diarizations kept (including failed ones) | 30 days, or until you delete them ([Data & privacy](/docs/data-privacy)) | | A5S Small V2 Streaming sessions at once | 5 | | Streaming session length | 30 minutes; up to 4 hours on request ([talk to sales](https://aircaps.ai/contact)) | | AirCaps Speaker ID-v1 clips | 0.5 s to 2 minutes (enrollments at least 1 s), up to 100 MB, one per request | | AirCaps Speaker ID-v1 speakers | 1,000 per account, 20 enrollments per speaker | | AirCaps Speaker ID-v1 requests (enrollments and identifications together) | 600 per minute | | Active API keys | 10 | Jobs beyond the processing limit wait as `queued`. Beyond the queue or request-rate limit you get `429` with a `Retry-After` header. Other endpoints are not rate limited. ## Pricing and higher limits Limits are set per account. For pricing, higher usage limits or concurrency, access to a model, or a dedicated deployment, [talk to sales](https://aircaps.ai/contact). > [card] [Talk to sales](https://aircaps.ai/contact) > > aircaps.ai/contact # Models Source: https://aircaps.ai/docs/models What each model does and when to use it.
ModelWhat it does
[**A5S Async v1**](#a5s-async-v1)\ `a5s-async-v1` Recorded audio (0.1 s to 10 h) → a transcript with speaker labels, word timestamps and subtitles.\ REST, async jobs
[**A5S Small V2 Streaming**](#a5s-small-v2-streaming)\ `a5s-small-v2-streaming` Live audio → captions as people speak, with optional speaker labels and names.\ WebSocket
[**AirCaps Diarization-v1**](#aircaps-diarization-v1)\ `aircaps-diarization-v1` Recorded audio (0.1 s to 6 h) → who spoke when: speaker segments as JSON or RTTM.\ REST, async jobs · enabled on request
[**AirCaps Speaker ID-v1**](#aircaps-speaker-id-v1)\ `aircaps-speaker-id-v1` A clip (0.5 s to 2 min) → a score for each of your enrolled speakers.\ REST, synchronous · enabled on request
## A5S Async v1 `a5s-async-v1` · REST · [batch guide](/docs/guides/async-transcription) · [benchmarks](https://aircaps.ai/research/a5s-async) * Recorded audio in any common format, from 0.1 s to 10 hours. * Speaker labels (`speaker_0`, `speaker_1`, …), word timestamps, utterances, SRT/VTT subtitles. * Optional key terms: up to 60 per transcript help it recognize names and jargon (`keyterms`, see [Key terms](/docs/guides/async-transcription#key-terms)). * Optional speaker names from your enrolled voices (`speaker_identification`, with AirCaps Speaker ID-v1 enabled). See [Speaker names](/docs/guides/async-transcription#speaker-names). * English only: `language_code` is `en` or a regional English code such as `en-US`, `en-GB` or `en-IN`; use the one that matches the speakers' locale or accent. * Text is verbatim and lowercase without punctuation (apostrophes are kept, as in "let's"); disfluencies ("uh", "um") are kept. ## A5S Small V2 Streaming `a5s-small-v2-streaming` · WebSocket · [streaming guide](/docs/guides/streaming) · [benchmarks](https://aircaps.ai/research/a5s-small-v2) > [!NOTE] > > Formerly A5S v2 Streaming, with id `a5s-v2-streaming`. The old id still works for now: a session or request that names it gets a `model_deprecated` warning (in `ready.warnings` on the WebSocket). It will be removed soon, so use `a5s-small-v2-streaming`. * Live 16 kHz mono PCM16 audio in, partial and final text out as people speak. * Punctuated, cased text. * Optional live speaker labels (`diarization: true`): utterances and words labelled `speaker_0`, `speaker_1`, …. See [Speaker labels](/docs/guides/streaming#speaker-labels). * Optional live speaker names from your enrolled voices (`speaker_identification`, with speaker labels on and AirCaps Speaker ID-v1 enabled). See [Speaker names](/docs/guides/streaming#speaker-names). * Optional key terms to bias recognition toward names and jargon (`custom_vocabulary`, see [Key terms](/docs/guides/streaming#key-terms)). * English only: the start message's `language` accepts only `en`. * Sessions last up to 30 minutes (up to 4 hours on request). ## AirCaps Diarization-v1 `aircaps-diarization-v1` · REST · [diarization guide](/docs/guides/diarization) · [benchmarks](https://aircaps.ai/research/diarization) · enabled on request * Who spoke when, with no transcript: speaker segments (`speaker_0`, `speaker_1`, …) with millisecond times, overlapping speech included, plus each speaker's speaking time. * Finds the number of speakers itself. Works from the voices, not the words: no `language_code`. * Recorded audio in any common format, from 0.1 s to 6 hours. Results as JSON or [RTTM](/docs/api-reference/diarization-rttm). * Enabled per account: [talk to sales](https://aircaps.ai/contact) for access. ## AirCaps Speaker ID-v1 `aircaps-speaker-id-v1` · REST, synchronous · [speaker ID guide](/docs/guides/speaker-id) · [evaluation](https://aircaps.ai/research/speaker-id) · enabled on request * Recognize enrolled people by voice. Enroll each person with one or more clips, then send a clip and get back a score for every enrolled speaker, best first. * Also names the speakers of A5S Async v1 transcripts (`speaker_identification` on [create a transcript](/docs/guides/async-transcription#speaker-names)) and of A5S Small V2 Streaming sessions with speaker labels ([live speaker names](/docs/guides/streaming#speaker-names)). * Synchronous: one clip per request, 0.5 s to 2 minutes; the scores are the response. No jobs, no polling. * You set the threshold: scores are similarities from -1 to 1, with no accept/reject decision. The [guide](/docs/guides/speaker-id#scores-and-thresholds) has measured score ranges to start from. * The whole clip is compared as given. Crop clips to one person's speech first (for example with [AirCaps Diarization-v1](/docs/guides/diarization) or a transcript's utterances). * Works from the voice, not the words: no `language_code`. * Enabled per account: [talk to sales](https://aircaps.ai/contact) for access. ## Choosing | You have | Use | | - | - | | Recordings, uploads, meeting files | A5S Async v1 | | A live microphone or call that needs captions now | A5S Small V2 Streaming | | Live audio that needs speaker labels as it happens | A5S Small V2 Streaming with `diarization: true` | | The most accurate speaker-labelled transcript of a recording | A5S Async v1 | | Only who spoke when (speaker segments, no text), or speakers for a transcript you already have | AirCaps Diarization-v1 | | Which known person is speaking in a clip (names instead of `speaker_0`, `speaker_1`) | AirCaps Speaker ID-v1 | ## Languages The transcription models support English today: `language_code` (A5S Async v1) takes `en` or a regional English code (`en-US`, `en-GB`, `en-IN`, …), and `language` (A5S Small V2 Streaming) takes `en`. **Multilingual support is planned and will be available soon.** AirCaps Diarization-v1 and AirCaps Speaker ID-v1 work from the voice and take no language. ## Access Accounts can use A5S Async v1 and A5S Small V2 Streaming by default; an account may be limited to one of them. AirCaps Diarization-v1 and AirCaps Speaker ID-v1 are enabled per account on request. Requests to a model your account can't use return `403 model_access_denied` (streaming: an `error` message with that code, then close code `4403`). `GET /v1/account` shows `enabled` for each model. To get access, [talk to sales](https://aircaps.ai/contact). `GET /v1/models` lists the models. # Quickstart Source: https://aircaps.ai/docs/quickstart A first request to each model. ## Get an API key Sign in at [console.aircaps.ai](https://console.aircaps.ai), open **API keys**, and create a key. It is shown once. ```bash theme={null} export AIRCAPS_API_KEY="aircaps_sk_..." ``` ## A5S Async v1: transcribe a recording **1. Submit audio** ```bash theme={null} # audio_url: a public or presigned URL (or "file_id" from an upload, below) # language_code: optional, "en" or the English variant that matches the speakers ("en-US", "en-GB", "en-IN", ...) curl https://api.aircaps.com/v1/transcripts \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "a5s-async-v1", "audio_url": "https://example.com/meeting.mp3", "language_code": "en"}' ``` The response is a transcript with `"status": "queued"` or `"processing"` and an `id` like `tr_01k6z...`. **2. Get the result** ```bash theme={null} # poll until status is "completed" (or "error"); or pass "webhook_url" when you submit curl https://api.aircaps.com/v1/transcripts/tr_01k6z... \ -H "Authorization: Bearer $AIRCAPS_API_KEY" ``` A local file in Python: ```python theme={null} # pip install requests # AIRCAPS_API_KEY=aircaps_sk_... python transcribe.py meeting.mp3 import os, sys, time, requests API = "https://api.aircaps.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} def call(method, path, **kw): r = requests.request(method, API + path, headers=HEADERS, timeout=60, **kw) if r.status_code >= 400: raise SystemExit(r.json()["error"]["message"]) return r.json() # 1. Upload straight to storage upload = call("POST", "/files/upload-url") with open(sys.argv[1], "rb") as f: requests.put(upload["upload_url"], data=f, timeout=600).raise_for_status() # 2. Transcribe job = call("POST", "/transcripts", json={ "model": "a5s-async-v1", "file_id": upload["id"], "language_code": "en", # optional # "webhook_url": "https://...", # optional: get notified instead of polling # "speaker_identification": True, # optional: names from enrolled voices (AirCaps Speaker ID-v1) }) # 3. Wait for the result while job["status"] in ("queued", "processing"): time.sleep(3) job = call("GET", f"/transcripts/{job['id']}") if job["status"] == "error": raise SystemExit(job["error"]["message"]) for u in job["utterances"]: print(f"[{u['start']:7.1f}s] {u['speaker']}: {u['text']}") ``` ## A5S Small V2 Streaming: live captions ```python theme={null} # pip install websockets==15.0.1 # AIRCAPS_API_KEY=aircaps_sk_... python stream.py audio.wav (mono 16 kHz 16-bit PCM WAV) import asyncio, json, os, sys, wave import websockets async def main(path): headers = {"Authorization": f"Bearer {os.environ['AIRCAPS_API_KEY']}"} async with websockets.connect("wss://api.aircaps.com/v1/realtime", additional_headers=headers) as ws: await ws.send(json.dumps({ "type": "start", "providers": ["a5sv2"], "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1, # the only audio format "language": "en", "diarization": True, # optional: live speaker labels })) while (event := json.loads(await ws.recv()))["type"] != "ready": # wait for ready before sending audio if event["type"] == "error": raise SystemExit(event["message"]) async def send_audio(): with wave.open(path, "rb") as audio: while chunk := audio.readframes(320): # 20 ms per frame, in real time await ws.send(chunk) await asyncio.sleep(0.02) await ws.send(json.dumps({"type": "finish"})) # final transcript, then "finished" sender = asyncio.create_task(send_audio()) async for message in ws: # until the server closes the session event = json.loads(message) if event["type"] == "transcript" and event["is_final"]: print(event["text"]) # the whole transcript so far await sender asyncio.run(main(sys.argv[1])) ``` ## AirCaps Diarization-v1: who spoke when ```bash theme={null} # Start a diarization (audio_url or file_id; optional: webhook_url, metadata) curl https://api.aircaps.com/v1/diarizations \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "aircaps-diarization-v1", "audio_url": "https://example.com/meeting.mp3"}' # Poll until status is "completed": speakers, and segments of {speaker, start, end} curl https://api.aircaps.com/v1/diarizations/dia_01k72... \ -H "Authorization: Bearer $AIRCAPS_API_KEY" # The same segments as RTTM curl https://api.aircaps.com/v1/diarizations/dia_01k72.../rttm \ -H "Authorization: Bearer $AIRCAPS_API_KEY" ``` ## AirCaps Speaker ID-v1: recognize enrolled voices ```bash theme={null} # 1. Create a speaker (name and metadata are optional) curl https://api.aircaps.com/v1/speakers \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Ana García"}' # 2. Enroll a clip of that person speaking alone: raw audio bytes as the body, 1 s to 2 min curl https://api.aircaps.com/v1/speakers/spk_01k73.../enrollments \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: audio/wav" \ --data-binary @ana.wav # 3. Identify a clip: a score for every enrolled speaker, best first (optional: speaker_ids, top_k) curl "https://api.aircaps.com/v1/speakers/identify?top_k=3" \ -H "Authorization: Bearer $AIRCAPS_API_KEY" \ -H "Content-Type: audio/wav" \ --data-binary @clip.wav ``` To name the speakers of a transcript or a live stream instead, add `"speaker_identification": true` to the request ([transcripts](/docs/guides/async-transcription#speaker-names), [live streams](/docs/guides/streaming#speaker-names)). > [!NOTE] > > **Access:** AirCaps Diarization-v1 and AirCaps Speaker ID-v1 are enabled per account; without access, requests return `403 model_access_denied`. [Talk to sales](https://aircaps.ai/contact) to get access. > > **Language:** A5S Async v1 and A5S Small V2 Streaming transcribe English only today: `language_code` (batch) takes `en` or a regional English code such as `en-US`, `en-GB` or `en-IN`, and `language` (streaming) takes `en`. Multilingual support is planned and will be available soon. Next: [batch transcription](/docs/guides/async-transcription), [live streaming](/docs/guides/streaming), [speaker diarization](/docs/guides/diarization) or [speaker identification](/docs/guides/speaker-id).