Wavesift API v1
Base URL https://api.wavesift.com/v1
Prompt for an AI agent OpenAPI JSON
Format: JSON in camelCase, enum values in snake_case, time in ISO-8601 UTC, errors as RFC 7807 application/problem+json.
Section 1

Authentication

Server-side integration works through an API key bound to your account. The key looks like atk_… (68 characters), is sent in the X-Api-Key header on every request and replaces sign-in: every resource is created on behalf of your account and falls under its plan limits. The key is shown once at creation; we store only its hash. You can set an expiry, revoke it at any time and keep up to 5 active keys.

The order: register an account, sign in, create a key from the sign-in session. Creating and revoking keys works only with a Bearer token: such a request signed with another key gets 403.

Every new account automatically gets a 14-day trial without plan quotas. After 14 days the account moves to the base plan and the keys keep working. We can extend the trial or move you to a paid plan on our side.
The key is a password-grade secret: keep it in a secrets store, do not commit it, do not pass it in a query string. If it leaks, revoke it via DELETE /user/api-keys/{id} and create a new one.
HeaderWhen to useLifetime
X-Api-Key: atk_…The whole integration: transcriptions, summaries, presets, subscription.until expiresAt or revocation
Authorization: Bearer …Key management only. The token is issued by POST /auth/login.15 minutes
Cookie: refresh_tokenExtend the session without a password via POST /auth/refresh (curl: -c jar.txt -b jar.txt).httpOnly
POST/auth/register Account registration
no authenticationapplication/json

Same response as sign-in: a 15-minute access token and the user object.

Request body application/json
FieldTypeDescription
emailstringrequiredAccount login, must be unique.
passwordstringrequiredPassword, checked for complexity.
usernamestringoptionalDisplay name.
Headers
HeaderValue
Content-Typeapplication/jsonrequired

No authentication required. Body: JSON with the fields from the “Parameters” tab.

200 application/json
FieldTypeDescription
accessTokenstringJWT for the Authorization: Bearer, valid for 15 minutes.
userUserThe created account, see User.

Along with the response the server sets the httpOnly cookie refresh_token.

CodeWhenWhat to do
400Email already taken or the password fails validation; the reason is in detail.Fix the data.
Requestcurl
curl -sS https://api.wavesift.com/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"ops@example.com","password":"…","username":"ops"}'
Response200 OKapplication/json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs…",
  "user": {
    "id": "01a06f11-…",
    "email": "ops@example.com",
    "username": "ops",
    "role": "client",
    "status": "active",
    "createdAt": "2026-09-05T11:30:02.114Z",
    "updatedAt": "2026-09-05T11:30:02.114Z"
  }
}
Error400 Bad Requestapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "<reason: email already taken or weak password>"
}
POST/auth/login Sign in and Bearer token
no authenticationapplication/json

Sign-in is needed for key management. Along with the response the server sets the httpOnly cookie refresh_token; the session can be extended without a password via POST /auth/refresh with this cookie.

Request body application/json
FieldTypeDescription
emailstringrequiredAccount email.
passwordstringrequiredPassword.
Headers
HeaderValue
Content-Typeapplication/jsonrequired

To keep the cookie for POST /auth/refresh, add to curl -c jar.txt, and to the following requests -b jar.txt.

200 application/json
FieldTypeDescription
accessTokenstringJWT for the Authorization: Bearer, valid for 15 minutes.
userUserThe account, see User.
CodeWhenWhat to do
401Wrong email or password.Check the credentials.
Requestcurl
curl -sS https://api.wavesift.com/v1/auth/login \
  -H "Content-Type: application/json" \
  -c jar.txt \
  -d '{"email":"ops@example.com","password":"…"}'
Response200 OKapplication/json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs…",
  "user": {
    "id": "01a06f11-…",
    "email": "ops@example.com",
    "username": "ops",
    "role": "client",
    "status": "active",
    "createdAt": "…",
    "updatedAt": "…"
  }
}
Error401 Unauthorizedapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401
}
POST/user/api-keys Create an API key
Authorization: Bearerapplication/json

Create a key for the integration. The apiKey field in the response is shown only this once; afterwards only the prefix is available. Take the token for this request from POST /auth/login.

Request body application/json
FieldTypeDescription
namestringrequiredKey name. Returned in the response as partnerName.
expiresAtdatetimeoptionalExpiry, ISO-8601 UTC. Without it the key never expires.
Headers
HeaderValue
AuthorizationBearer <accessToken>required
Content-Typeapplication/jsonrequired

A request signed with an API key instead of Bearer gets 403.

200 application/json
FieldTypeDescription
apiKeystringThe full key value. Save it right away; it is never shown again.
keyApiKeyKey metadata, see ApiKey: id to revoke it, keyPrefix, partnerName, expiresAt, isActive.
CodeWhenWhat to do
400expiresAt is in the past.Fix the date.
403The request is signed with an API key instead of Bearer.Sign in via POST /auth/login.
409Already 5 active keys.Revoke a key you no longer need.
Requestcurl
curl -sS https://api.wavesift.com/v1/user/api-keys \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Production","expiresAt":"2027-09-01T00:00:00Z"}'
Response200 OKapplication/json
{
  "key": {
    "id": "01a06f1c-…",
    "ownerUserId": "01a06f11-…",
    "ownerEmail": "ops@example.com",
    "partnerName": "Production",
    "keyPrefix": "atk_4b0zgyEB",
    "bypassLimits": false,
    "isActive": true,
    "expiresAt": "2027-09-01T00:00:00Z",
    "revokedAt": null,
    "lastUsedAt": null,
    "createdAt": "2026-09-05T11:31:40.902Z"
  },
  "apiKey": "atk_4b0zgyEB…"
}
Error403 Forbiddenapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
  "title": "Forbidden",
  "status": 403,
  "detail": "<reason: key management requires Bearer>"
}
GET/user/api-keys List keys
X-Api-Key or Bearer

Your keys, newest first. No raw values here, only the prefix and dates.

No parameters.

Headers
HeaderValue
X-Api-Keyatk_…or Bearer
200 application/json
FieldTypeDescription
itemsApiKey[]Array of ApiKey, newest first.
CodeWhenWhat to do
401The key or token is missing, invalid, revoked or expired.Check authentication.
Requestcurl
curl -sS https://api.wavesift.com/v1/user/api-keys \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "items": [
    {
      "id": "01a06f1c-…",
      "ownerUserId": "…",
      "ownerEmail": "ops@example.com",
      "partnerName": "Production",
      "keyPrefix": "atk_4b0zgyEB",
      "bypassLimits": false,
      "isActive": true,
      "expiresAt": "2027-09-01T00:00:00Z",
      "revokedAt": null,
      "lastUsedAt": "2026-09-05T12:02:11.008Z",
      "createdAt": "…"
    }
  ]
}
Error401 Unauthorizedapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401
}
DELETE/user/api-keys/{id} Revoke a key
Authorization: Bearer

Takes effect immediately and irreversibly. The response is the same key object with revokedAt and isActive: false.

Path
FieldTypeDescription
iduuidrequiredid of the key from the list, not the key itself.
Headers
HeaderValue
AuthorizationBearer <accessToken>required

No body.

200 application/json

Object ApiKey with isActive: false and a filled-in revokedAt.

CodeWhenWhat to do
403The request is signed with an API key.Sign in via POST /auth/login.
404The key is not yours or does not exist.Check id.
Requestcurl
curl -sS -X DELETE https://api.wavesift.com/v1/user/api-keys/$KEY_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response200 OKapplication/json
{
  "id": "01a06f1c-…",
  "partnerName": "Production",
  "keyPrefix": "atk_4b0zgyEB",
  "isActive": false,
  "revokedAt": "2026-09-05T12:10:33.510Z",
  … the remaining fields as in the ApiKey model
}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/user/me Check the key
X-Api-Key

Returns the account the key acts on behalf of. Handy as the first request of an integration and as a liveness check.

No parameters.

Headers
HeaderValue
X-Api-Keyatk_…required
200 application/json

Object User.

CodeWhenWhat to do
401The key is missing, invalid, revoked or expired.Check the key or create a new one.
Requestcurl
curl -sS https://api.wavesift.com/v1/user/me \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "id": "01a06f11-…",
  "email": "ops@example.com",
  "username": "ops",
  "role": "client",
  "status": "active",
  "createdAt": "…",
  "updatedAt": "…"
}
Error401 Unauthorizedapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401
}
Section 2

Prompt for a developer's AI agent

If Claude Code, Cursor or another agent writes the integration, hand it this text as the task. It compresses the whole contract into one instruction: base address, authentication, the five steps, polling, channel handling, error codes and implementation requirements. The agent takes the language and framework from your project. No need to paste the key; it is read from the environment variable WAVESIFT_API_KEY.

Prompt blockWhat it pins down
BASE URL · SPECAPI address and the link to the OpenAPI JSON as the source of truth.
AUTH · FORMATHeader X-Api-Key from the environment variable, camelCase, ISO-8601, RFC 7807.
FLOW 1–5Upload, wait, transcript, summary, wait for the summary. Channels: left = participant 1, right = participant 2.
WEBHOOKSRegistering an endpoint, signature verification, deduplication, polling only as a fallback.
ERROR HANDLINGWhat to do for each code, backoff for 5xx, X-Correlation-Id in the logs.
REQUIREMENTSKeep the original audio, idempotency, tests, a one-file CLI.
OpenAPI JSON
PromptEnglish · plain text
Integrate our audio recordings (mono or two-channel) with the Wavesift HTTP API. Build a small, well-tested client module (or CLI) in this project's primary language and follow the contract below exactly — do not invent endpoints or fields.

BASE URL: https://api.wavesift.com/v1
SPEC: the exact machine-readable contract (request/response schemas, enums, status codes) is the OpenAPI document at https://api.wavesift.com/docs/public/openapi.json (the human-readable contract is at /docs). Fetch it first and treat it as the source of truth where this text is less specific; do not use endpoints that are not in it.
AUTH: send header `X-Api-Key: <key>` on every request. Read the key from the env var WAVESIFT_API_KEY. Never log it, never commit it, never put it in a URL. Keys are created once by a human from a login session (POST /auth/login → Bearer token → POST /user/api-keys `{ "name", "expiresAt"? }` → `apiKey` is shown once); the integration itself must not create or rotate keys.
FORMAT: JSON with camelCase fields; enum values are lowercase snake_case strings; timestamps are ISO-8601 UTC. Errors are RFC 7807 `application/problem+json` with `status` and `detail`.

FLOW (one recording = one transcription):
1. Upload: POST /transcriptions as multipart/form-data with field `system_file` (the recording, mono or stereo; accepted extensions: wav, flac, mp3, m4a, m4b, aac, ogg, opus, wma, webm, mp4, mkv), optional `title`, `external_id` (our call id; the server dedupes on it — re-uploading with the same external_id returns the existing transcription instead of creating another one) and `language` (ISO code like "uk", or "auto"; default auto). Response 200 is the transcription object with `id`, `kind: "upload"` and `status: "queued"`. Body limit is 6 GB per request. Batch alternative: POST /transcriptions/bulk with up to 50 file fields → `{ items: [{ index, fileName, transcription | null, error | null }] }` (partial success is normal).
   Channel handling: recordings can be mono or two-channel. When a two-channel file carries a different speaker on each channel, the server splits it automatically: LEFT channel → `speakerLabel: "SPEAKER_1"`, RIGHT channel → `"SPEAKER_2"`; these labels are fixed once the transcription is `completed`. For mono, or when both channels carry the same mix, there is no split: right after `completed` the segments have `speakerLabel: null`, then the server runs voice diarization and fills `SPEAKER_N` in order of first appearance; `audioDeletedAt` becoming non-null signals that this step is done.
2. Wait: poll GET /transcriptions/{id} every 5–10 s (or subscribe to GET /transcriptions/{id}/events, text/event-stream, events `status`/`progress`). `status` is one of queued | processing | completed | failed. On `failed` read `errorMessage` and re-upload; do not retry in a tight loop.
3. Read the transcript: GET /transcriptions/{id}/segments → `{ items: [{ id, start, end, text, source, speakerUserId, speakerLabel }] }` ordered by `start` (seconds). Or GET /transcriptions/{id}/markdown for text/markdown with timecodes and speaker tags like `[OTHER-SPEAKER_1]` (404 until completed).
4. Summary: POST /transcriptions/{id}/summaries with JSON `{ "presetId": "<guid>" }`. Only valid when the transcription is `completed` (otherwise 409). List presets with GET /summary-presets → `{ items: [{ id, name, prompt, isGlobal, ... }] }`; the ready-made two-speaker call preset is named "Розбір дзвінка КЦ" (id 0198c0de-0000-7000-8000-000000000005). To use our own instructions create a preset once: POST /summary-presets `{ "name", "prompt" }` (the server prepends a short preamble describing the transcript format and appends the transcript itself; do not include the transcript in the prompt; the summary is written in the transcript's language unless the prompt says otherwise) and reuse its id.
5. Wait for the summary: poll GET /summaries/{summaryId} every 5 s until `status: "completed"`, then read `markdown`. `failed` carries `errorMessage`.

WEBHOOKS (preferred over polling): register once with POST /user/webhooks `{ "url": "https://…", "events": ["transcription.completed", "transcription.diarized", "transcription.failed", "summary.completed", "summary.failed"] }`; the response contains `secret` (shown once, store it with the API key). Every delivery is a POST with JSON `{ id, event, occurredAt, data }` and headers `X-Wavesift-Event`, `X-Wavesift-Delivery`, `X-Wavesift-Timestamp`, `X-Wavesift-Signature: v1=<hex>` where hex = HMAC-SHA256(secret, "<timestamp>.<raw body>"). Verify the signature on the raw body with a constant-time compare, reject timestamps older than 5 minutes, respond 2xx within 10 s and process asynchronously, dedupe by delivery `id` (a delivery may be repeated; retries come after 1 min, 5 min, 30 min, 2 h, 12 h). For mono recordings speaker labels arrive with `transcription.diarized`, not with `transcription.completed`. Keep polling as a fallback only.

OTHER ENDPOINTS: GET /user/me (identity check), GET /transcriptions?page=&limit=&status=&kind=&q= (own history, `{ items, totalCount }`; `kind` is `upload` for single-file uploads and `meeting` when both mic_file and system_file were sent), GET /transcriptions/{id}/summaries (summary history), DELETE /transcriptions/{id} (204), GET /subscription/current (plan and usage; 0 in the plan and null in the remaining fields mean unlimited).

ERROR HANDLING: 400 → fix the request; 401 → the key is missing/invalid/expired, stop and alert a human (do not retry); 403 → wrong resource id or wrong auth method; 404 → unknown id or transcript not ready yet; 409 → business rule (transcription not completed, plan limit on file size / minutes / presets) — wait or surface it; 413 → file over 6 GB (the body may be an HTML page from the proxy); 429 → rate limit (10/min per IP on /auth/*, 300/min per account elsewhere), wait for the seconds in the `Retry-After` header and never poll more often than every 5 s; 5xx → retry with exponential backoff (max 5 attempts) and include the `X-Correlation-Id` response header in logs. Set the correlation header yourself (a UUID per call) so we can trace requests.

REQUIREMENTS: keep our original audio (the server deletes source audio after processing); store `transcriptionId`, `summaryId`, transcript markdown and summary markdown against our call record; send our call id as `external_id` so retries never create duplicates; unit-test the JSON mapping and the status machine with recorded fixtures; add a CLI/command that runs the whole flow for one file and prints the summary.
Section 3

Quick start

One conversation goes through five steps. The key was created in section 1; your integration does the rest. Every step is shown as a single command. Variables: $API_KEY your key, $ID transcription id from step 2, $SUMMARY_ID summary id from step 5.

1

Check the key

The first request of an integration. Returns the account the key acts on behalf of. 401 means the key is invalid, revoked or expired.

RequestGET /user/me
curl -sS https://api.wavesift.com/v1/user/me \
  -H "X-Api-Key: $API_KEY"
2

Upload a recording

A stereo file, one channel per participant. The response comes back immediately with status queued and id.

RequestPOST /transcriptions
curl -sS https://api.wavesift.com/v1/transcriptions \
  -H "X-Api-Key: $API_KEY" \
  -F "system_file=@call-48213.wav" \
  -F "language=uk"
3

Wait for the status

Poll every 5–10 seconds until completed or failed. Instead of polling there is an SSE stream GET /transcriptions/{id}/events.

RequestGET /transcriptions/{id}
curl -sS https://api.wavesift.com/v1/transcriptions/$ID \
  -H "X-Api-Key: $API_KEY"
4

Fetch the transcript

Segments with timecodes and speaker labels. The same text as markdown is available via GET /transcriptions/{id}/markdown.

RequestGET /transcriptions/{id}/segments
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/segments \
  -H "X-Api-Key: $API_KEY"
5

Request a summary

Pass presetId: the ready-made «Розбір дзвінка КЦ» or your own preset. The result is fetched from GET /summaries/{summaryId}, once the status is completed.

RequestPOST /transcriptions/{id}/summaries
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/summaries \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"presetId":"0198c0de-0000-7000-8000-000000000005"}'
Section 4

Channels and diarization

A channel is not a separate file but a track inside the audio file: mono has one, stereo has two (L and R). What is recorded in the channels determines how the server tells who is speaking.

RecordingWhat is insideWhat the server doesspeakerLabel
monoOne channel, all participants together.Transcribes as a single stream, then runs voice diarization.null right after completed, then SPEAKER_1, SPEAKER_2, … in order of first appearance.
stereo, one participant per channelTelephony or a softphone where each side records into its own channel: L one participant, R the other.Detects that the channels alternate, splits the file into two tracks and transcribes each separately. No voice diarization needed; the audio is deleted right after the transcription.SPEAKER_1 = L, SPEAKER_2 = R. Labels are fixed already at completed.
stereo with the same mixMono duplicated into two channels, or a microphone recording where both channels hear everyone.Processed as mono.As for mono.
two filesmic_file and system_file, each participant in their own file.Transcribes both and merges them by time; voice diarization only for system_file. kind will be meeting.source: "you" for mic_file, source: "other" for system_file.
L (0)
Participant 1 → speakerLabel: SPEAKER_1
R (1)
Participant 2 → speakerLabel: SPEAKER_2
1 stream
null → SPEAKER_N after diarization

How “one participant per channel” is detected: the server listens to the first 60 seconds in half-second windows and counts when only the left channel is active, only the right, or both. The split is enabled if each channel is active at least 15% of the time, in 70% of the active windows only one channel speaks, and overlap is below 20%. If one channel is almost silent (under 3%), the file is treated as mono.

If your telephony can record each side into its own channel, turn it on: you get stable labels without diarization, and the result is ready right at completed. In the examples on this page participant 1 is the operator and participant 2 the customer, but that is only an illustration.

Voice diarization

For mono and for a single stream, after completed the server queues a separate diarization job. The agent runs the recording through pyannote speaker-diarization 3.1, gets “who speaks when” turns and maps them onto the already finished transcript segments. Speakers are numbered in order of first appearance: SPEAKER_1, SPEAKER_2, … After that speakerLabel in the segments is filled in and the markdown document is regenerated, so both views match. It takes about as long as the transcription itself.

For mono do not read the labels right after completed, they are still empty. Wait for the transcription.diarized webhook or the audioDeletedAt in GET /transcriptions/{id}: it is filled in once diarization has finished and the original is deleted.

Voice prints

A voice print is a stored fingerprint of a specific person's voice with a label, for example “Maryna, operator”. You upload 5–30 seconds of that person's clean speech, the agent computes a vector representation of the voice (an embedding) and stores it in your account. During voice diarization every detected speaker is compared with your voice prints; on a match (cosine similarity of 0.5 or higher), instead of SPEAKER_N in speakerLabel the print's label is substituted, and if the print is linked to a user, it also fills in speakerUserId. Voice prints do not affect channel splitting.

MethodPathDescription
POST/voice-prints/uploadmultipart: audio (file, 5–30 s), label (label), subject_user_id (optional). Response 202 with status: "pending"; the agent computes the embedding in the background.
GET/voice-prints/{id}Poll until status: "ready" or "failed" (reason in errorMessage).
GET/voice-printsAll voice prints of the account.
PUT/voice-prints/{id}Change the label or the linked user.
DELETE/voice-prints/{id}Delete a voice print; later diarizations no longer see it.
Requestcurl · voice print
curl -sS https://api.wavesift.com/v1/voice-prints/upload \
  -H "X-Api-Key: $API_KEY" \
  -F "audio=@maryna-sample.wav" \
  -F "label=Maryna, operator"
Section 5

Transcriptions

One request per conversation. The response comes back immediately with status queued; processing is done by a pool of agents, so the result is fetched separately. Formats by file extension: wav, flac, mp3, m4a, m4b, aac, ogg, opus, wma, webm, mp4, mkv; any other extension returns 400. We recommend WAV or FLAC, 16 kHz or higher.

POST/transcriptions Upload a recording
X-Api-Keymultipart/form-databody up to 6 GB

Upload one recording. The response is the transcription object with status queued, then its id used in all subsequent requests.

Form fields multipart/form-data
FieldTypeDescription
system_filefilerequired *The audio file. * At least one of system_file / mic_file.
mic_filefileoptionalThe second track when the two participants are already recorded separately. Not sent in the stereo scenario.
titlestringoptionalAny name for the recording.
external_idstringoptionalYour own id of the recording, up to 200 characters. Makes the upload idempotent: a repeated request with the same external_id returns the already created transcription; the file is not stored again and no minutes are charged.
languagestringoptionalISO code (uk, en, ru, …) or auto. Default is auto.
mic_offset_msintegeroptionalTrack start offset in ms when the tracks do not start at the same time.
system_offset_msintegeroptionalThe same for the system track.
Headers
HeaderValue
X-Api-Keyatk_…required
Content-Typemultipart/form-datais set by curl

The example on the right shows a stereo file. For two mono files send mic_file=@operator.wav and system_file=@client.wav instead of a single system_file. Body up to 6 GB per request.

200 application/json

Object Transcription. Right after the upload only the id, the status and what you sent are filled in; the other fields get their values during processing.

FieldTypeMeaning
iduuidId for all subsequent requests.
kindenumupload for a single file, meeting for two tracks.
statusenumAlways queued.
hasSystemTrack · hasMicTrackbooleanWhich tracks were received.
systemFileSizeBytes · micFileSizeBytesinteger | nullFile sizes in bytes.
createdAtdatetimeUpload time.
CodeWhenWhat to do
400No file or unsupported extension.Fix the request.
409The plan's file size or minutes limit is exceeded.Check GET /subscription/current.
413Body over 6 GB. The response may come from the proxy with an HTML body.Compress to FLAC or split the file.
Requestcurl · stereo file
curl -sS https://api.wavesift.com/v1/transcriptions \
  -H "X-Api-Key: $API_KEY" \
  -F "system_file=@call-2026-09-05-1432.wav" \
  -F "title=Call #48213, operator Ivanova" \
  -F "language=uk"
Response200 OKapplication/json
{
  "id": "01a06f3e-7c1a-7b52-9d2e-3f0c1a9d8e11",
  "kind": "upload",
  "status": "queued",
  "title": "Call #48213, operator Ivanova",
  "language": "uk",
  "detectedLanguage": null,
  "whisperModel": null,
  "durationSeconds": null,
  "hasMicTrack": false,
  "hasSystemTrack": true,
  "micFileSizeBytes": null,
  "systemFileSizeBytes": 18432044,
  "progressStage": null,
  "progressPercent": null,
  "errorMessage": null,
  "startedAt": null,
  "completedAt": null,
  "audioDeletedAt": null,
  "createdAt": "2026-09-05T11:32:07.412Z"
}
Error409 Conflictapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "Conflict",
  "status": 409,
  "detail": "Monthly audio limit exceeded: 3012.4/3000 min used"
}
POST/transcriptions/bulk Bulk upload
X-Api-Keymultipart/form-dataup to 50 files

Up to 50 files at once, each becomes a separate transcription following the stereo scenario; title equals the file name. One failed file does not fail the batch: the response carries error next to the specific file.

Form fields multipart/form-data
FieldTypeDescription
any namefilerequired1 to 50 files. Field names do not matter; every file becomes a separate transcription.
languagestringoptionalShared by all files in the batch.
Headers
HeaderValue
X-Api-Keyatk_…required
Content-Typemultipart/form-datais set by curl
200 application/json
FieldTypeDescription
items[].indexintegerPosition of the file in the request.
items[].fileNamestringFile name, also the transcription's title .
items[].transcriptionTranscription | nullThe created transcription, see the model. null if the file was rejected.
items[].errorstring | nullWhy this particular file was rejected.
CodeWhenWhat to do
400No files, or more than 50.Split the batch.

Plan limits on an individual file do not fail the whole batch with a 4xx; they land in error of the corresponding item.

Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/bulk \
  -H "X-Api-Key: $API_KEY" \
  -F "language=uk" \
  -F "f1=@call-48213.wav" \
  -F "f2=@call-48214.wav"
Response200 OKapplication/json
{
  "items": [
    {
      "index": 0,
      "fileName": "call-48213.wav",
      "transcription": { "id": "01a06f40-…", "status": "queued",  },
      "error": null
    },
    {
      "index": 1,
      "fileName": "call-48214.wav",
      "transcription": null,
      "error": "Monthly audio limit exceeded: 3012.4/3000 min used"
    }
  ]
}
Error400 Bad Requestapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "<reason: no files or more than 50>"
}

Processing status

Poll every 5–10 seconds until a terminal status, or subscribe to SSE. Processing time depends on the recording length and the agent queue.

Timing guide: roughly 1.5 seconds of processing per minute of audio plus the wait in the queue. A one-hour recording is usually ready 2–3 minutes after an agent picks it up.
queued processing completedorfailed
statusValueWhat to do
queuedQueued, no agent has picked it up yet.Wait.
processingBeing processed. Watch progressStage / progressPercent.Wait, show progress.
completedDone. Filled in: durationSeconds, detectedLanguage, completedAt.Fetch the transcript, request summaries.
failedFailed, the reason is in errorMessage.Upload the file again.
GET/transcriptions/{id} Transcription status
X-Api-Key

Current state of the transcription. This is the main polling request: repeat it every 5–10 seconds until a terminal status.

Path
FieldTypeDescription
iduuidrequiredid from the upload response.
Headers
HeaderValue
X-Api-Keyatk_…required

No body. Do not poll more often than every 5 seconds.

200 application/json

Object Transcription. Fields worth watching while polling:

FieldTypeMeaning
statusenumTerminal values are completed and failed.
progressStagestring | nullStage string for display, for example TRANSCRIBE_START, DONE.
progressPercentinteger | null0–100 during processing.
errorMessagestring | nullReason when failed.
completedAtdatetime | nullWhen the transcription finished.
audioDeletedAtdatetime | nullWhen the original was deleted. For a single stream it also means voice diarization has finished.
CodeWhenWhat to do
403Transcription of another account.Check id and the key.
404Unknown id.Check id.
Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/$ID \
  -H "X-Api-Key: $API_KEY"
Response200 OKcompleted
{
  "id": "01a06f3e-7c1a-7b52-9d2e-3f0c1a9d8e11",
  "kind": "upload",
  "status": "completed",
  "title": "Call #48213, operator Ivanova",
  "language": "uk",
  "detectedLanguage": "uk",
  "whisperModel": "large-v3-turbo",
  "durationSeconds": 187.4,
  "hasMicTrack": false,
  "hasSystemTrack": true,
  "micFileSizeBytes": null,
  "systemFileSizeBytes": 18432044,
  "progressStage": "DONE",
  "progressPercent": 100,
  "errorMessage": null,
  "startedAt": "2026-09-05T11:32:31.006Z",
  "completedAt": "2026-09-05T11:34:12.887Z",
  "audioDeletedAt": "2026-09-05T11:34:12.901Z",
  "createdAt": "2026-09-05T11:32:07.412Z"
}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/transcriptions/{id}/events Status as a stream (SSE)
X-Api-Keytext/event-stream

The first event is a snapshot status, then progress/status until a terminal status, after which the stream closes. You can reconnect at any time. stage is an arbitrary stage string for display.

Path
FieldTypeDescription
iduuidrequiredThe transcription to watch.
Headers
HeaderValue
X-Api-Keyatk_…required

In curl disable buffering with -N to see events immediately.

200 text/event-stream
eventdata fieldsDescription
statustype, status, stage, percent, messageStatus snapshot. The first event is always this one; the last carries the terminal status.
progresstype, stage, percentProcessing progress; status here is null.
CodeWhenWhat to do
403Transcription of another account.Check id and the key.
404Unknown id.Check id.
Requestcurl
curl -sN https://api.wavesift.com/v1/transcriptions/$ID/events \
  -H "X-Api-Key: $API_KEY"
Response200 OKtext/event-stream
event: status
data: {"type":"status","status":"processing","stage":null,"percent":null,"message":null}

event: progress
data: {"type":"progress","status":null,"stage":"TRANSCRIBE_START","percent":42,"message":null}

event: status
data: {"type":"status","status":"completed","stage":"DONE","percent":100,"message":null}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/transcriptions/{id}/segments Transcript as segments
X-Api-Key

Segments are ordered by start time. start and end in seconds from the beginning of the recording.

Path
FieldTypeDescription
iduuidrequiredA transcription with status completed.
Headers
HeaderValue
X-Api-Keyatk_…required

No body.

200 application/json

{ items: Segment[] }. Key segment fields:

FieldDescription
sourceyou — the track mic_file; other — the track system_file (in the stereo scenario both speakers are here).
speakerLabelStereo: SPEAKER_1 = left channel, SPEAKER_2 = right. Single stream: null until voice diarization finishes, then SPEAKER_N in order of first appearance.
speakerUserIdAlways null (used by voice profiles in other products).
CodeWhenWhat to do
403Someone else's transcription.Check id and the key.
404Unknown id.Check id.
Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/segments \
  -H "X-Api-Key: $API_KEY"
Response200 OKstereo scenario
{
  "items": [
    {
      "id": "01a06f45-…",
      "start": 0.42,
      "end": 3.91,
      "text": "Good afternoon, Ortex company, operator Maryna speaking.",
      "source": "other",
      "speakerUserId": null,
      "speakerLabel": "SPEAKER_1"
    },
    {
      "id": "01a06f45-…",
      "start": 4.10,
      "end": 7.55,
      "text": "Hello, I am calling about order forty-eight thousand two hundred thirteen.",
      "source": "other",
      "speakerUserId": null,
      "speakerLabel": "SPEAKER_2"
    }
  ]
}
Error403 Forbiddenapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
  "title": "Forbidden",
  "status": 403
}
GET/transcriptions/{id}/markdown Transcript as text
X-Api-Keytext/markdown

The same transcript as ready-made text with timecodes and speaker labels. This is the text the server passes to the model for summaries. After voice diarization the document is regenerated, so the labels here and in the segments match.

Path
FieldTypeDescription
iduuidrequiredA transcription with status completed.
Headers
HeaderValue
X-Api-Keyatk_…required

To save to a file, add to curl -o call-48213.md.

200 text/markdown; charset=utf-8

A line of the form **[hh:mm:ss] [SOURCE-SPEAKER_N]** text per segment. SOURCE is OTHER for system_file and YOU for mic_file.

CodeWhenWhat to do
404The transcript is not ready yet (status is not completed) or unknown id.Wait for completed.
Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/markdown \
  -H "X-Api-Key: $API_KEY" \
  -o call-48213.md
Response200 OKtext/markdown
# Transcript

**[00:00:00] [OTHER-SPEAKER_1]** Good afternoon, Ortex company, operator Maryna speaking.

**[00:00:04] [OTHER-SPEAKER_2]** Hello, I am calling about order forty-eight thousand two hundred thirteen.

**[00:00:07] [OTHER-SPEAKER_1]** Let me check. Could you tell me the recipient's surname, please?
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/transcriptions List transcriptions
X-Api-Key

List of your transcriptions, newest first. All parameters are optional.

Query
FieldTypeDescription
pageintegeroptionalPage number, from 1.
limitintegeroptionalPage size, up to 1000.
statusenumoptionalqueued | processing | completed | failed.
kindenumoptionalupload for a single file, meeting for two.
qstringoptionalSearch by name.
external_idstringoptionalExact match of your recording id.
Headers
HeaderValue
X-Api-Keyatk_…required

Quote query parameters in curl so the shell does not swallow &.

200 application/json
FieldTypeDescription
itemsTranscription[]A page of transcriptions, see the model.
totalCountintegerTotal count with the filters applied.
CodeWhenWhat to do
400Unknown value of status or kind.Fix the filter.
Requestcurl
curl -sS "https://api.wavesift.com/v1/transcriptions?page=1&limit=20&status=completed&kind=upload&q=48213" \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "items": [
    { "id": "01a06f3e-…", "kind": "upload", "status": "completed", "title": "Call #48213, operator Ivanova",  }
  ],
  "totalCount": 1
}
Error400 Bad Requestapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "<reason: unknown status or kind value>"
}
DELETE/transcriptions/{id} Delete a transcription
X-Api-Key

Delete a transcription together with its segments, markdown and all its summaries. Irreversible.

Path
FieldTypeDescription
iduuidrequiredThe transcription to delete.
Headers
HeaderValue
X-Api-Keyatk_…required

No body.

204 No Content

Empty response. Calling again for the same id returns 404.

CodeWhenWhat to do
403Someone else's transcription.Check id and the key.
404Unknown id.Check id.
Requestcurl
curl -sS -X DELETE https://api.wavesift.com/v1/transcriptions/$ID \
  -H "X-Api-Key: $API_KEY"
Response204 No Contentno body
(empty response)
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
Section 6

Summaries

A preset is a saved prompt with a name. There are five global server presets, among them «Розбір дзвінка КЦ» for two-channel conversations (topic, outcome, timeline with timecodes, operator score on five criteria, risks, next steps), and your account's own presets. A summary is generated by presetId, available only for a transcription with status completed, asynchronous. The model is chosen by the account's plan and returned in ollamaModel. There is no quota on the number of summaries.

GET/summary-presets Available presets
X-Api-Key

Enabled global presets plus your own.

No parameters.

Global presets ids of the form 0198c0de-0000-7000-8000-00000000000N
NName
…0001Повне самарі
…0002Ключові моменти
…0003Задачі та фікси
…0004Пояснення функціоналу
…0005Розбір дзвінка КЦ
Headers
HeaderValue
X-Api-Keyatk_…required
200 application/json

{ items: SummaryPreset[] }. For global ones isGlobal: true, they cannot be edited.

CodeWhenWhat to do
401Key missing or invalid.Check the key.
Requestcurl
curl -sS https://api.wavesift.com/v1/summary-presets \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "items": [
    { "id": "0198c0de-0000-7000-8000-000000000001", "name": "Повне самарі",         "prompt": "…", "isGlobal": true,  "isEnabled": true, "sortOrder": 1,  },
    { "id": "0198c0de-0000-7000-8000-000000000002", "name": "Ключові моменти",       "prompt": "…", "isGlobal": true,  "isEnabled": true, "sortOrder": 2,  },
    { "id": "0198c0de-0000-7000-8000-000000000003", "name": "Задачі та фікси",       "prompt": "…", "isGlobal": true,  "isEnabled": true, "sortOrder": 3,  },
    { "id": "0198c0de-0000-7000-8000-000000000004", "name": "Пояснення функціоналу", "prompt": "…", "isGlobal": true,  "isEnabled": true, "sortOrder": 4,  },
    { "id": "0198c0de-0000-7000-8000-000000000005", "name": "Розбір дзвінка КЦ",     "prompt": "…", "isGlobal": true,  "isEnabled": true, "sortOrder": 5,  },
    { "id": "01a06f5b-…",                           "name": "Call quality review",    "prompt": "…", "isGlobal": false, "isEnabled": true, "sortOrder": 0,  }
  ]
}
Error401 Unauthorizedapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401
}
POST/summary-presets Create your own preset
X-Api-Keyapplication/json

Describe the task and the response format in the prompt. Do not paste the transcript: the server prepends a short preamble (explaining the timecode and speaker label format) and appends the transcript text itself. By default the response is written in the transcript's language; to get the summary in another language, say so in the prompt.

Request body application/json
FieldTypeDescription
namestringrequiredPreset name.
promptstringrequiredInstructions for the model without the transcript text.
Editing and deleting

PUT /summary-presets/{id} with the same body { "name", "prompt" }. DELETE /summary-presets/{id}204. Both work only with your own presets; global ones return 403. Already generated summaries keep a snapshot of the name and prompt, so editing a preset does not change history.

Headers
HeaderValue
X-Api-Keyatk_…required
Content-Typeapplication/jsonrequired
200 application/json

Object SummaryPreset with isGlobal: false. Its id then goes into presetId.

CodeWhenWhat to do
400Empty name or prompt.Fill in the fields.
409The plan's custom presets limit is reached.Delete a preset you no longer need.
Requestcurl
curl -sS https://api.wavesift.com/v1/summary-presets \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Call quality review",
    "prompt": "You are reviewing the quality of a call. SPEAKER_1 is the operator, SPEAKER_2 is the customer. Give: 1) the topic in one sentence; 2) whether the issue was resolved (yes/no/partially); 3) an operator score 1–5 for politeness, accuracy and script adherence, with quotes; 4) the next step. Answer in English, markdown."
  }'
Response200 OKapplication/json
{
  "id": "01a06f5b-…",
  "name": "Call quality review",
  "prompt": "You are reviewing the quality of a call…",
  "isGlobal": false,
  "isEnabled": true,
  "sortOrder": 0,
  "createdAt": "…",
  "updatedAt": "…"
}
Error409 Conflictapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "Conflict",
  "status": 409,
  "detail": "<reason: custom presets limit of the plan>"
}
POST/transcriptions/{id}/summaries Request a summary
X-Api-Keyapplication/json

Queue a summary. The response comes back immediately with status queued; the result is fetched with a separate request by the summary id id.

Path
FieldTypeDescription
iduuidrequiredA transcription with status completed.
Request body application/json
FieldTypeDescription
presetIduuidrequiredA global or your own preset from GET /summary-presets.
Headers
HeaderValue
X-Api-Keyatk_…required
Content-Typeapplication/jsonrequired
200 application/json

Object Summary with status queued. Its id is summaryId for subsequent requests.

CodeWhenWhat to do
403Someone else's or a disabled preset, someone else's transcription.Check presetId and id.
404Unknown preset or transcription.Check the ids.
409The transcription is not yet completed.Wait for the status.
Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/summaries \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"presetId":"01a06f5b-…"}'
Response200 OKapplication/json
{
  "id": "01a06f7a-…",
  "transcriptionId": "01a06f3e-…",
  "presetId": "01a06f5b-…",
  "presetName": "Call quality review",
  "ollamaModel": "gemma4:12b",
  "status": "queued",
  "markdown": null,
  "errorMessage": null,
  "createdAt": "2026-09-05T11:41:20.008Z",
  "completedAt": null
}
Error409 Conflictapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "Conflict",
  "status": 409,
  "detail": "Transcription 01a06f3e-7c1a-7b52-9d2e-3f0c1a9d8e11 has no transcript yet (status: processing)"
}
GET/summaries/{summaryId} Summary status and text
X-Api-Key

Poll until status: "completed"; the result is in markdown. The statuses are the same: queuedprocessingcompleted | failed.

Path
FieldTypeDescription
summaryIduuidrequiredid from the summary request response.
Headers
HeaderValue
X-Api-Keyatk_…required

No body. Poll every 5 seconds.

200 application/json

Object Summary. After completed the markdown contains the finished text; on failed the reason is in errorMessage.

CodeWhenWhat to do
403Summary of someone else's transcription.Check summaryId and the key.
404Unknown summaryId.Check summaryId.
Requestcurl
curl -sS https://api.wavesift.com/v1/summaries/$SUMMARY_ID \
  -H "X-Api-Key: $API_KEY"
Response200 OKcompleted
{
  "id": "01a06f7a-…",
  "transcriptionId": "01a06f3e-…",
  "presetId": "01a06f5b-…",
  "presetName": "Call quality review",
  "ollamaModel": "gemma4:12b",
  "status": "completed",
  "markdown": "## Topic\nOrder #48213 status check.\n\n## Resolved\nYes.\n\n## Operator score\n- Politeness: 5 …",
  "errorMessage": null,
  "createdAt": "2026-09-05T11:41:20.008Z",
  "completedAt": "2026-09-05T11:42:03.551Z"
}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/summaries/{summaryId}/events Summary as a stream (SSE)
X-Api-Keytext/event-stream

Status and tokens as they are generated. After a reconnect the tokens already sent are not replayed, so fetch the finished text via GET /summaries/{summaryId}.

Path
FieldTypeDescription
summaryIduuidrequiredThe summary to watch.
Headers
HeaderValue
X-Api-Keyatk_…required

In curl disable buffering with -N.

200 text/event-stream
eventdata fieldsDescription
statustype, status, messageSummary status snapshot.
tokentype, tokenA chunk of text as it is generated.
CodeWhenWhat to do
403Summary of someone else's transcription.Check summaryId and the key.
404Unknown summaryId.Check summaryId.
Requestcurl
curl -sN https://api.wavesift.com/v1/summaries/$SUMMARY_ID/events \
  -H "X-Api-Key: $API_KEY"
Response200 OKtext/event-stream
event: status
data: {"type":"status","status":"processing","token":null,"message":null,"stage":null,"percent":null}

event: token
data: {"type":"token","status":null,"token":"## Topic\n","message":null,"stage":null,"percent":null}

event: status
data: {"type":"status","status":"completed","token":null,"message":null,"stage":null,"percent":null}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
GET/transcriptions/{id}/summaries Summary history of a transcription
X-Api-Key

All summaries of one transcription, newest first.

Path
FieldTypeDescription
iduuidrequiredThe transcription.
Headers
HeaderValue
X-Api-Keyatk_…required
200 application/json

{ items: Summary[] }, newest first.

CodeWhenWhat to do
403Someone else's transcription.Check id and the key.
404Unknown id.Check id.
Requestcurl
curl -sS https://api.wavesift.com/v1/transcriptions/$ID/summaries \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "items": [
    { "id": "01a06f7a-…", "presetName": "Call quality review", "status": "completed", "markdown": "…",  }
  ]
}
Error404 Not Foundapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404
}
Section 7

Subscription

GET/subscription/current Plan, limits and usage
X-Api-Key

Current plan, limits and usage for the calendar month. In the plan 0 means “unlimited”; then the corresponding …Remaining equals null. All available plans: GET /subscription/plans.

No parameters.

Headers
HeaderValue
X-Api-Keyatk_…required
200 application/json

Object Subscription with a nested Plan. The example on the right: an account on trial.

FieldDescription
endsAtEnd of the trial. After the switch to the base plan it is null.
audioMinutesUsed · audioMinutesRemainingMinutes in the period and the remainder; null, when there is no limit.
customPresetsCount · customPresetsRemainingCustom presets and the remainder.
CodeWhenWhat to do
401Key missing or invalid.Check the key.
Requestcurl
curl -sS https://api.wavesift.com/v1/subscription/current \
  -H "X-Api-Key: $API_KEY"
Response200 OKapplication/json
{
  "subscriptionId": "01a06f12-…",
  "status": "active",
  "startsAt": "2026-09-05T11:30:02.114Z",
  "endsAt": "2026-09-19T11:30:02.114Z",
  "plan": {
    "id": "0198c0de-1000-7000-8000-000000000003",
    "code": "trial",
    "name": "Trial",
    "isEnabled": true,
    "isDefault": false,
    "priceMonthly": 0,
    "currency": "USD",
    "monthlyAudioMinutes": 0,
    "maxCustomPresets": 0,
    "maxUploadFileSizeMb": 0,
    "maxSeats": 1,
    "preferredSummaryModel": "gemma4:12b",
    "preferredWhisperModel": "large-v3-turbo",
    "sortOrder": 0,
    "isTrial": true,
    "trialDays": 14
  },
  "periodStart": "2026-09-01T00:00:00Z",
  "periodEnd": "2026-10-01T00:00:00Z",
  "audioMinutesUsed": 412.7,
  "audioMinutesRemaining": null,
  "transcriptionsCount": 96,
  "summariesCount": 88,
  "customPresetsCount": 1,
  "customPresetsRemaining": null
}
Error401 Unauthorizedapplication/problem+json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401
}
Section 8

Webhooks

Instead of polling, the server itself sends a POST to your address when an event happens. One account can have up to 10 endpoints, each with its own set of events and signing secret. Management works both with X-Api-Key and with Bearer.

EventWhendata
transcription.completedThe transcription moved to completed. For mono the speaker labels are still empty.Object Transcription without the file sizes.
transcription.diarizedVoice diarization finished, speakerLabel are filled in, the markdown is regenerated.The same Transcription object.
transcription.failedProcessing failed for good; the reason is in errorMessage.The same Transcription object.
summary.completedThe summary is ready, the text is in markdown.Object Summary.
summary.failedGeneration failed; the reason is in errorMessage.The same Summary object.
pingTest delivery from POST /user/webhooks/{id}/test.{ "endpointId", "message" }

Every delivery is a POST with a JSON body and the headers X-Wavesift-Event, X-Wavesift-Delivery (delivery id), X-Wavesift-Timestamp (unix time in seconds) and X-Wavesift-Signature. Signature: v1=HMAC-SHA256(secret, "<timestamp>.<body>") as hex, where the body is taken byte for byte as received.

Verify the signature on the raw body before JSON parsing, compare in constant time and reject requests whose X-Wavesift-Timestamp is older than 5 minutes. The secret is shown only at creation and at rotate-secret.

Respond with 2xx within 10 seconds and process asynchronously. Otherwise the delivery is retried after 1 min, 5 min, 30 min, 2 h and 12 h, after which it gets the status failed. The same event may arrive twice; deduplicate by the delivery id . The history of the last 50 deliveries with the response code and error is available in GET /user/webhooks/{id}/deliveries.

The address must be https:// on a public domain. Private networks and hosts without a domain are rejected. Create the endpoint first, then POST …/test: within 10 seconds a ping arrives, which is a convenient way to debug signature verification.
MethodPathDescription
POST/user/webhooksCreate: { "url", "events": [...], "description"? }. The response contains secret, it is shown only here. 400 invalid address or event, 409 already 10 endpoints.
GET/user/webhooksYour endpoints: address, events, isActive, failureStreak, lastStatusCode.
PUT/user/webhooks/{id}Change the address, events, description, or disable via isActive: false.
DELETE/user/webhooks/{id}Delete; undelivered events are dropped. 204.
POST/user/webhooks/{id}/testQueue a ping. Response 202 with the delivery object.
POST/user/webhooks/{id}/rotate-secretA new secret; the old one stops working immediately.
GET/user/webhooks/{id}/deliveriesThe last 50 deliveries: event, status, attempts, code and error of the last attempt.
Requestcurl · create an endpoint
curl -sS https://api.wavesift.com/v1/user/webhooks \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://crm.example.com/wavesift",
    "events": ["transcription.completed", "transcription.diarized", "summary.completed", "transcription.failed", "summary.failed"],
    "description": "CRM"
  }'
Response200 OKapplication/json
{
  "endpoint": {
    "id": "01a07001-…",
    "url": "https://crm.example.com/wavesift",
    "description": "CRM",
    "events": ["transcription.completed", "transcription.diarized", "summary.completed", "transcription.failed", "summary.failed"],
    "isActive": true,
    "failureStreak": 0,
    "lastDeliveryAt": null,
    "lastStatusCode": null,
    "createdAt": "2026-09-06T12:40:11.204Z",
    "updatedAt": "2026-09-06T12:40:11.204Z"
  },
  "secret": "whsec_9f2c…"
}
Deliverywhat arrives at your address
POST /wavesift HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Wavesift-Webhooks/1.0
X-Wavesift-Event: transcription.completed
X-Wavesift-Delivery: 01a07012-…
X-Wavesift-Timestamp: 1788698463
X-Wavesift-Signature: v1=3f9a1c…

{
  "id": "01a07012-…",
  "event": "transcription.completed",
  "occurredAt": "2026-09-06T12:41:03.118+00:00",
  "data": {
    "id": "01a06f3e-…",
    "externalId": "48213",
    "kind": "upload",
    "status": "completed",
    "title": "Call #48213, operator Ivanova",
    "language": "uk",
    "detectedLanguage": "uk",
    "durationSeconds": 187.4,
    "errorMessage": null,
    "completedAt": "2026-09-06T12:41:03.101+00:00",
    "audioDeletedAt": "2026-09-06T12:41:03.115+00:00",
    "createdAt": "2026-09-06T12:38:07.412+00:00"
  }
}
Signature verificationNode.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWavesift(rawBody, headers, secret) {
  const ts = headers["x-wavesift-timestamp"];
  const given = headers["x-wavesift-signature"].replace("v1=", "");
  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
}
Reference

Data models

Every object the API returns, with field types. uuid is a UUID string, datetime is an ISO-8601 UTC string, enum is a string from a fixed set. The mark | null means the field can be empty.

Transcription

POST /transcriptions · GET /transcriptions/{id} · GET /transcriptions
FieldTypeDescription
iduuidTranscription id, used in every child request.
kindenumupload for a single file, meeting when both mic_file and system_file.
statusenumqueued | processing | completed | failed.
titlestring | nullName from the title, in a batch it equals the file name.
externalIdstring | nullYour id from the external_id; in bulk uploads always null.
languagestringRequested language: ISO code or auto.
detectedLanguagestring | nullDetected language, filled in after completed.
whisperModelstring | nullThe recognition model that processed the recording, for example large-v3-turbo.
durationSecondsnumber | nullRecording duration in seconds, after completed. Plan minutes are charged by it.
hasMicTrackbooleanWhether the request included mic_file.
hasSystemTrackbooleanWhether the request included system_file.
micFileSizeBytesinteger | nullSize of mic_file in bytes.
systemFileSizeBytesinteger | nullSize of system_file in bytes.
progressStagestring | nullCurrent stage string for display, for example TRANSCRIBE_START, DONE.
progressPercentinteger | nullProgress 0–100 during processing.
errorMessagestring | nullReason when failed.
startedAtdatetime | nullWhen an agent picked the recording up.
completedAtdatetime | nullWhen the transcription finished.
audioDeletedAtdatetime | nullWhen the original audio was deleted. For a single stream it also means voice diarization has finished.
createdAtdatetimeUpload time.

Segment

GET /transcriptions/{id}/segments
FieldTypeDescription
iduuidSegment id.
startnumberStart in seconds from the beginning of the recording.
endnumberEnd in seconds.
textstringRecognised text of the segment.
sourceenumyou — the track mic_file; other — the track system_file.
speakerUserIduuid | nullAlways null.
speakerLabelstring | nullSPEAKER_1, SPEAKER_2, … Stereo: 1 = left channel, 2 = right. Single stream: null until diarization finishes.

Summary

POST /transcriptions/{id}/summaries · GET /summaries/{summaryId}
FieldTypeDescription
iduuidSummary id (summaryId).
transcriptionIduuidThe transcription the summary was generated for.
presetIduuidThe preset the summary was requested with.
presetNamestringSnapshot of the preset name at request time.
ollamaModelstringThe model chosen by the account's plan, for example gemma4:12b.
statusenumqueued | processing | completed | failed.
markdownstring | nullThe finished summary text after completed.
errorMessagestring | nullReason when failed.
createdAtdatetimeRequest time.
completedAtdatetime | nullCompletion time.

SummaryPreset

GET /summary-presets · POST /summary-presets
FieldTypeDescription
iduuidId used as presetId.
namestringPreset name.
promptstringInstructions for the model.
isGlobalbooleantrue for server presets; they cannot be edited.
isEnabledbooleanWhether it is available for generation.
sortOrderintegerDisplay order.
createdAt · updatedAtdatetimeCreated and last updated.

ApiKey

POST /user/api-keys · GET /user/api-keys · DELETE /user/api-keys/{id}
FieldTypeDescription
iduuidKey id, used to revoke it.
ownerUserIduuidOwning account.
ownerEmailstringOwner email.
partnerNamestringName from the name at creation.
keyPrefixstringFirst characters of the key for identification, for example atk_4b0zgyEB.
bypassLimitsbooleanInternal flag for bypassing plan limits.
isActivebooleanfalse after revocation.
expiresAtdatetime | nullExpiry; null for a key that never expires.
revokedAtdatetime | nullRevocation time.
lastUsedAtdatetime | nullLast request made with this key.
createdAtdatetimeCreation time.

User

POST /auth/register · POST /auth/login · GET /user/me
FieldTypeDescription
iduuidAccount id.
emailstringLogin.
usernamestring | nullDisplay name.
roleenumFor integrations client.
statusenumAccount state; the working state is active.
createdAt · updatedAtdatetimeCreated and last updated.

Subscription

GET /subscription/current
FieldTypeDescription
subscriptionIduuidSubscription id.
statusenumSubscription state; the working state is active.
startsAtdatetimeSubscription start.
endsAtdatetime | nullEnd of the trial. After the switch to the base plan it is null.
planPlanPlan object, see below.
periodStart · periodEnddatetimeThe calendar month usage is counted for.
audioMinutesUsednumberAudio minutes in the period.
audioMinutesRemainingnumber | nullRemaining; null, when there is no limit.
transcriptionsCountintegerTranscriptions in the period.
summariesCountintegerSummaries in the period.
customPresetsCountintegerCustom presets.
customPresetsRemaininginteger | nullRemaining; null, when there is no limit.

Plan

plan field · GET /subscription/plans
FieldTypeDescription
iduuidPlan id.
code · namestringCode and name, for example trial / Trial.
isEnabled · isDefaultbooleanWhether the plan is active and whether it is the base plan after the trial.
priceMonthly · currencynumber · stringMonthly price and currency.
monthlyAudioMinutesintegerAudio minutes per month; 0 means unlimited.
maxCustomPresetsintegerCustom presets, 0 means unlimited.
maxUploadFileSizeMbintegerMaximum file size in MB, 0 means unlimited.
maxSeatsintegerNumber of seats.
preferredSummaryModelstringThe plan's summary model, for example gemma4:12b.
preferredWhisperModelstringThe plan's recognition model, for example large-v3-turbo.
isTrial · trialDaysboolean · integerWhether it is a trial and its length in days.
sortOrderintegerDisplay order.

Webhook

POST /user/webhooks · GET /user/webhooks · PUT /user/webhooks/{id}
FieldTypeDescription
iduuidEndpoint id.
urlstringAddress that receives the server's POST. Only https://.
descriptionstring | nullAny label.
eventsstring[]Events the endpoint subscribes to: transcription.completed, transcription.diarized, transcription.failed, summary.completed, summary.failed.
isActivebooleanA disabled endpoint receives no new deliveries.
failureStreakintegerHow many recent attempts failed in a row; reset after a success.
lastDeliveryAt · lastStatusCodedatetime | null · integer | nullTime and HTTP status code of the last attempt.
createdAt · updatedAtdatetimeCreated and last updated.

WebhookDelivery

POST /user/webhooks/{id}/test · GET /user/webhooks/{id}/deliveries
FieldTypeDescription
iduuidDelivery id; the same as in X-Wavesift-Delivery and in the id of the body.
eventTypestringEvent, for example transcription.completed.
statusenumpending | delivered | failed.
attemptsintegerHow many attempts were made, at most 6.
nextAttemptAtdatetimeWhen the next attempt happens, while the status is pending.
lastStatusCode · lastErrorinteger | null · string | nullHTTP status code and error of the last attempt.
createdAt · deliveredAtdatetime · datetime | nullWhen the event happened and when the delivery was accepted.

Problem

all errors · application/problem+json · RFC 7807
FieldTypeDescription
typestringLink to the status description in RFC 9110.
titlestringStatus name, for example Conflict.
statusintegerHTTP status code.
detailstring | nullHuman-readable reason when the server knows it.
Reference

Error codes

Errors are returned as application/problem+json (RFC 7807). Every response carries the X-Correlation-Id; you may send your own UUID in the same header and it comes back unchanged. Quote it when you contact us.

CodeWhenWhat to do
400Invalid body or parameters: no file, unsupported extension, unknown status/kind, malformed JSON.Fix the request.
401Missing X-Api-Key, the key is invalid, revoked or expired; for Bearer the token has expired (15 min).Check the key or create a new one; for Bearer sign in again. Do not retry in a loop.
403The resource belongs to another account; key management via an API key; a global or someone else's preset.Check id and the authentication method.
404No such transcription / summary / preset / key, or the transcript is not ready yet (for /markdown).Check id or wait for completed.
409Business rule violation: a summary for an unfinished transcription, the plan limit on file size, minutes or number of presets, 5 active keys.Wait for the status or check the limits.
410The original audio is already deleted (only for /audio/{track}).The transcript and summaries stay available.
413Body over 6 GB. The response may come from the proxy with an HTML body.Split or compress (FLAC).
429Request limit exceeded: 10 per minute per IP for /auth/*, 300 per minute per account for everything else.Wait the number of seconds given in the Retry-After header and retry. Do not poll more often than every 5 seconds.
5xxFailure on our side.Retry after a pause and send us the X-Correlation-Id.
Reference

Limits and retention

RuleMeaning
14-day trialFor every new account: plan quotas do not apply (file size, minutes, number of custom presets); only the technical 6 GB per request limit remains. The end date is in endsAt; after it the account moves to the base plan and endsAt becomes null.
Audio minutesCharged by recording duration once the transcription completes. Uploading beyond the limit returns 409 or a separate error in the batch response.
Audio is not retainedFor a stereo recording split by channels the original file is deleted right after the transcription completes; for a single stream after voice diarization; in any case no later than 7 days. The deletion time is visible in audioDeletedAt. Keep your own copy.
Transcripts and summariesKept without a time limit until you delete them via DELETE /transcriptions/{id}.
Body limit6 GB per request regardless of the plan.
KeysUp to 5 active at once; expiry is optional.
Requests300 per minute per account (sliding window), 10 per minute per IP for /auth/*. Exceeding it returns 429 with the header Retry-After.
WebhooksUp to 10 endpoints per account; 6 delivery attempts over roughly 15 hours.
Reference

The full scenario as one script

File run-call.sh: bash + curl + jq. Upload a stereo recording, wait, fetch the transcript, request a summary with the «Розбір дзвінка КЦ» preset. The key is read from the environment variable WAVESIFT_API_KEY. It is one complete file, not a set of commands to paste one by one.

Filerun-call.sh · bash
#!/usr/bin/env bash
set -euo pipefail
API=https://api.wavesift.com/v1
FILE=${1:?path to the stereo wav}
PRESET_ID=${2:-0198c0de-0000-7000-8000-000000000005}

AUTH="X-Api-Key: ${WAVESIFT_API_KEY:?set WAVESIFT_API_KEY}"

ID=$(curl -sS "$API/transcriptions" -H "$AUTH" -F "system_file=@$FILE" -F "language=uk" \
  -F "title=$(basename "$FILE")" | jq -r .id)
echo "transcription: $ID"

while :; do
  S=$(curl -sS "$API/transcriptions/$ID" -H "$AUTH")
  STATUS=$(jq -r .status <<<"$S")
  echo "  $STATUS $(jq -r '.progressStage // ""' <<<"$S") $(jq -r '.progressPercent // ""' <<<"$S")"
  [[ $STATUS == completed ]] && break
  [[ $STATUS == failed ]] && { jq -r .errorMessage <<<"$S"; exit 1; }
  sleep 10
done

curl -sS "$API/transcriptions/$ID/markdown" -H "$AUTH" -o "$ID.transcript.md"

SUMMARY_ID=$(curl -sS "$API/transcriptions/$ID/summaries" -H "$AUTH" \
  -H "Content-Type: application/json" -d "{\"presetId\":\"$PRESET_ID\"}" | jq -r .id)

while :; do
  S=$(curl -sS "$API/summaries/$SUMMARY_ID" -H "$AUTH")
  STATUS=$(jq -r .status <<<"$S")
  [[ $STATUS == completed ]] && { jq -r .markdown <<<"$S" > "$ID.summary.md"; break; }
  [[ $STATUS == failed ]] && { jq -r .errorMessage <<<"$S"; exit 1; }
  sleep 5
done
echo "done: $ID.transcript.md, $ID.summary.md"
Copied