Theme
Meeting AI
Upload a meeting recording, poll until processing finishes, then read the speaker-labelled transcript, summary, decisions, and action items.
Authentication
Use a server-side developer API key with Meeting AI → Access, permission ID meetings. Access grants meetings:read and meetings:write. Restricted keys need this permission added explicitly. See API keys.
| Credential | Route prefix | Purpose |
|---|---|---|
| Developer API key | /v1/meetings | Server integrations documented here |
| User access JWT | /api/v1/meetings | VoiceLab dashboard |
Meeting AI is available to personal workspace keys only. Team workspace keys return 403 workspace_product_unavailable. Meetings belong to the account: a permitted key can retrieve meetings created by another key or in the web app, and meetings created with a key appear in the web Meeting AI list. Other accounts cannot access them; their IDs return 404 meeting_not_found.
Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /v1/meetings/capabilities | meetings:read | Supported languages and feature flags |
POST | /v1/meetings | meetings:write | Upload a recording; returns 202 |
GET | /v1/meetings | meetings:read | List meetings or refresh selected IDs |
GET | /v1/meetings/{id} | meetings:read | Poll status and read results |
PATCH | /v1/meetings/{id} | meetings:write | Rename a meeting |
DELETE | /v1/meetings/{id} | meetings:write | Delete a meeting |
POST | /v1/meetings/{id}/notes/generations | meetings:write | Regenerate notes after a notes failure |
Developer responses never include audio playback URLs. Audio download, transcript and notes editing, speaker renaming, translations, and webhooks are not available with API keys. Meeting AI has no completion webhooks; poll instead.
Check capabilities
List supported recording languages before uploading.
Requires meetings:read.
bash
curl --fail-with-body 'https://api.voicelab.uz/v1/meetings/capabilities' \
-H "Authorization: Bearer $VOICELAB_API_KEY"Returns 200. data lists supported languages with code, name, and native_name. Send one code as language, or omit it for automatic detection. Allow unknown fields in this response.
Create a meeting
Upload a recording for asynchronous transcription and notes.
Requires meetings:write.
Headers and fields
| Header | Required | Value |
|---|---|---|
Authorization | yes | Bearer <VOICELAB_API_KEY> |
Idempotency-Key | yes | UUID generated once per logical upload; reuse for retries |
Content-Type | yes | Multipart boundary generated by your HTTP client |
| Multipart field | Required | Value |
|---|---|---|
audio | yes | One audio file |
title | no | 1–160 characters, single line; defaults to the filename without its extension |
language | no | auto or a code from capabilities; default auto |
Send each field at most once. Unknown or repeated parts return 400 invalid_multipart. API-key uploads are always recorded as uploads; the web app's source field is not accepted.
The same idempotency key and content return the original meeting with Idempotent-Replayed: true and no second credit hold. Changing the audio, title, or language with that key returns 409 idempotency_key_reused. Keys are scoped per developer API key, separately from web app uploads.
Limits
| Limit | Value |
|---|---|
| Upload size | 500 MiB, or 524,288,000 bytes |
| Duration | 0.5 seconds to 24 hours |
| Formats | MP3, WAV, M4A/AAC, OGG/Opus, WebM/Opus, or FLAC |
| Pending meetings | 10 queued or running meetings per account |
| Write rate | Shared per-account budget for uploads and notes regeneration |
The API inspects the file contents and ignores the client-supplied content type. Write requests share their per-account rate limit with the Meeting AI web app, so dashboard activity and API traffic draw from the same budget. Reads have a separate per-account budget. When ten meetings are already queued or running, new uploads return 429 meeting_queue_full; wait for one to finish. Respect Retry-After on every 429.
Request examples
These examples run on your server. Set VOICELAB_API_KEY in the environment. Let the client generate the multipart boundary. Retain the generated UUID if you retry the upload.
bash
request_key=$(uuidgen)
curl --fail-with-body -i 'https://api.voicelab.uz/v1/meetings' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H "Idempotency-Key: $request_key" \
-F 'audio=@weekly-sync.mp3' \
-F 'title=Weekly sync' \
-F 'language=en'python
import os
import time
import uuid
import requests
api = "https://api.voicelab.uz/v1/meetings"
headers = {"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"}
request_key = str(uuid.uuid4())
with open("weekly-sync.mp3", "rb") as audio:
response = requests.post(
api,
headers={**headers, "Idempotency-Key": request_key},
files={"audio": ("weekly-sync.mp3", audio, "audio/mpeg")},
data={"title": "Weekly sync", "language": "en"},
timeout=900,
)
response.raise_for_status()
meeting = response.json()["meeting"]
while meeting["status"] in ("queued", "running"):
time.sleep(int(response.headers.get("Retry-After", "2")))
response = requests.get(f"{api}/{meeting['id']}", headers=headers, timeout=30)
response.raise_for_status()
meeting = response.json()["meeting"]
if meeting["status"] == "failed":
raise RuntimeError(meeting["failure"]["code"])
print(meeting["transcript"]["text"])
print(meeting["notes"]["summary"])ts
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const requestKey = randomUUID();
const form = new FormData();
form.append("audio", new Blob([new Uint8Array(await readFile("weekly-sync.mp3"))]), "weekly-sync.mp3");
form.append("title", "Weekly sync");
form.append("language", "en");
const response = await fetch("https://api.voicelab.uz/v1/meetings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VOICELAB_API_KEY}`,
"Idempotency-Key": requestKey,
},
body: form,
signal: AbortSignal.timeout(900_000),
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
console.log((await response.json()).meeting.id);Response and polling
Returns 202 Accepted, Location: /v1/meetings/{id}, and Retry-After: 2. Retries with the same key and content return the existing meeting:
json
{
"meeting": {
"id": "mtg_550e8400-e29b-41d4-a716-446655440000",
"title": "Weekly sync",
"status": "queued",
"capture_source": "upload",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z",
"audio": {
"filename": "weekly-sync.mp3",
"content_type": "audio/mpeg",
"size_bytes": 7340032,
"duration_ms": 1830000
},
"transcript": { "text": "", "language": "", "language_probability": 0, "segments": [] },
"notes": { "version": 0, "summary": "", "decisions": [], "action_items": [] },
"versions": [],
"processing": {
"transcription": { "status": "queued", "retryable": false },
"notes": { "status": "waiting", "retryable": false }
}
},
"async": true,
"request_id": "req_example"
}Poll GET /v1/meetings/{id} every two seconds, or follow Retry-After, until status is no longer queued or running:
status | Meaning | Next step |
|---|---|---|
queued | Waiting for the processor | Keep polling |
running | Transcription or notes in progress | Keep polling |
transcribed | Transcript saved; notes generation failed | Read the transcript; optionally regenerate notes |
ready | Transcript and notes complete | Read results |
failed | Processing failed; no transcript | Inspect failure.code; upload again if appropriate |
processing.transcription and processing.notes report each stage separately: waiting, queued, running, ready, or failed, with a retryable flag and a safe failure object when a stage fails. A meeting can return to queued or running with its transcript intact while notes are regenerated.
When the meeting is ready, read these fields:
| Field | Meaning |
|---|---|
transcript.text | Full transcript text |
transcript.language, transcript.language_probability | Detected or requested language and confidence |
transcript.segments | Timed segments with speaker_id, speaker, text, and words |
notes.summary | Meeting summary |
notes.decisions | Decisions, each with evidence.segment_ids |
notes.action_items | Action items with owner and evidence.segment_ids |
failure | code and message when processing fails |
List meetings
List the account's meetings, newest first.
Requires meetings:read. Optional limit defaults to 20 and accepts 1–50. Pass the returned opaque next_cursor as cursor for the next page. To refresh specific meetings, send ids as a comma-separated list of up to 50 meeting IDs instead of cursor. IDs that do not belong to the account are omitted.
bash
curl --fail-with-body 'https://api.voicelab.uz/v1/meetings?limit=20' \
-H "Authorization: Bearer $VOICELAB_API_KEY"Returns 200 with meetings in descending creation order and optional next_cursor. Invalid limit, cursor, or ids values return 400 invalid_meeting_query.
Get a meeting
Read meeting status, transcript, and notes.
Requires meetings:read. Returns 200 with meeting. Unknown meetings, deleted meetings, and meetings belonging to another account return 404 meeting_not_found.
bash
curl --fail-with-body 'https://api.voicelab.uz/v1/meetings/mtg_REPLACE_ME' \
-H "Authorization: Bearer $VOICELAB_API_KEY"Rename a meeting
Change a meeting's title.
Requires meetings:write. Send only title, 1–160 characters on one line. Unknown fields return 422 validation_error.
bash
curl --fail-with-body -X PATCH 'https://api.voicelab.uz/v1/meetings/mtg_REPLACE_ME' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"title":"Weekly sync — product"}'Returns 200 with the updated meeting.
Delete a meeting
Delete a meeting and its results.
Requires meetings:write. Returns 204 No Content. Deleting a queued or running meeting releases its credit hold. Deleted meetings also disappear from the web app.
bash
curl --fail-with-body -X DELETE 'https://api.voicelab.uz/v1/meetings/mtg_REPLACE_ME' \
-H "Authorization: Bearer $VOICELAB_API_KEY"Regenerate notes
Retry notes for a meeting whose notes generation failed.
Requires meetings:write. Only meetings with status transcribed qualify: the transcript exists and notes generation failed. Other meetings return 409 meeting_notes_not_retryable. Regeneration is billed again at the meeting's full duration and counts toward the key's credit limit and the shared write rate limit.
bash
curl --fail-with-body -X POST 'https://api.voicelab.uz/v1/meetings/mtg_REPLACE_ME/notes/generations' \
-H "Authorization: Bearer $VOICELAB_API_KEY"Returns 202 Accepted with meeting, Location: /v1/meetings/{id}, and Retry-After: 2. Poll the meeting as after an upload.
Billing and errors
Credits are reserved when the upload is accepted, prorated by recording duration at the configured per-minute rate and rounded up to a whole credit. They are debited once when processing succeeds. Processing failures and deletion before completion release the hold. Polling, listing, renaming, and deleting are not charged. Notes regeneration reserves and charges again. See pricing.
A key's credit limit includes its recorded billable usage and pending reservations across Meeting AI, Voice Isolator, and LLM requests. Uploads or regenerations that would exceed it return 402 api_key_credit_limit. Revoking a key prevents new access but does not cancel accepted meetings. Usage is attributed to the submitting key under meeting_ai in analytics.
| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_idempotency_key | Missing or non-UUID Idempotency-Key |
400 | invalid_multipart | Malformed form, missing audio, or unknown/repeated parts |
400 | invalid_meeting_query | Invalid limit, cursor, or ids |
401 | invalid_api_key | Missing, invalid, expired, disabled or revoked API key |
402 | insufficient_credits | Account balance cannot cover the reservation |
402 | api_key_credit_limit | Request would exceed the key's credit limit |
403 | insufficient_scope | Key lacks meetings:read or meetings:write |
403 | workspace_product_unavailable | Key belongs to a team workspace |
403 | workspace_mismatch, workspace_forbidden | Workspace header or membership does not match the key |
404 | meeting_not_found | Unknown, deleted, or another account's meeting |
409 | idempotency_key_reused | Same key sent with different content |
409 | meeting_processing | The original request with this key is still in progress |
409 | meeting_notes_not_retryable | Notes regeneration requested for a meeting that is not transcribed |
409 | meeting_not_ready | Operation requires completed processing |
413 | audio_too_large | Upload exceeds 500 MiB |
415 | unsupported_audio_format | File contents are not a supported audio format |
422 | invalid_audio | Unreadable audio, or duration outside 0.5 seconds–24 hours |
422 | invalid_title, invalid_language, validation_error | Invalid title, language, or rename body |
429 | rate_limited | Shared per-account request budget exceeded |
429 | meeting_queue_full | Ten meetings are already queued or running |
429 | meeting_upload_capacity | Upload capacity is temporarily busy |
503 | meeting_billing_unavailable | Pricing unavailable; nothing was queued |
503 | meeting_feature_unavailable, meeting_worker_unavailable | Meeting AI or its processor is unavailable; nothing was queued |
503 | developer_auth_unavailable | API key verification is temporarily unavailable |
504 | meeting_request_timeout | Request timed out; check status before retrying |
500 | meeting_internal_error | Unexpected failure |
Honor Retry-After for 429 and 503 responses. Retry transient upload failures with the original idempotency key and identical content; after a timeout or lost response, the retry returns the existing meeting instead of creating a duplicate. Do not retry authorization or validation failures unchanged. Keep request_id for support. See errors and limits.