Skip to content

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.

CredentialRoute prefixPurpose
Developer API key/v1/meetingsServer integrations documented here
User access JWT/api/v1/meetingsVoiceLab 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 ​

MethodPathScopePurpose
GET/v1/meetings/capabilitiesmeetings:readSupported languages and feature flags
POST/v1/meetingsmeetings:writeUpload a recording; returns 202
GET/v1/meetingsmeetings:readList meetings or refresh selected IDs
GET/v1/meetings/{id}meetings:readPoll status and read results
PATCH/v1/meetings/{id}meetings:writeRename a meeting
DELETE/v1/meetings/{id}meetings:writeDelete a meeting
POST/v1/meetings/{id}/notes/generationsmeetings:writeRegenerate 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 ​

GEThttps://api.voicelab.uz/v1/meetings/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 ​

POSThttps://api.voicelab.uz/v1/meetings

Upload a recording for asynchronous transcription and notes.

Requires meetings:write.

Headers and fields ​

HeaderRequiredValue
AuthorizationyesBearer <VOICELAB_API_KEY>
Idempotency-KeyyesUUID generated once per logical upload; reuse for retries
Content-TypeyesMultipart boundary generated by your HTTP client
Multipart fieldRequiredValue
audioyesOne audio file
titleno1–160 characters, single line; defaults to the filename without its extension
languagenoauto 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 ​

LimitValue
Upload size500 MiB, or 524,288,000 bytes
Duration0.5 seconds to 24 hours
FormatsMP3, WAV, M4A/AAC, OGG/Opus, WebM/Opus, or FLAC
Pending meetings10 queued or running meetings per account
Write rateShared 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:

statusMeaningNext step
queuedWaiting for the processorKeep polling
runningTranscription or notes in progressKeep polling
transcribedTranscript saved; notes generation failedRead the transcript; optionally regenerate notes
readyTranscript and notes completeRead results
failedProcessing failed; no transcriptInspect 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:

FieldMeaning
transcript.textFull transcript text
transcript.language, transcript.language_probabilityDetected or requested language and confidence
transcript.segmentsTimed segments with speaker_id, speaker, text, and words
notes.summaryMeeting summary
notes.decisionsDecisions, each with evidence.segment_ids
notes.action_itemsAction items with owner and evidence.segment_ids
failurecode and message when processing fails

List meetings ​

GEThttps://api.voicelab.uz/v1/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 ​

GEThttps://api.voicelab.uz/v1/meetings/{id}

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 ​

PATCHhttps://api.voicelab.uz/v1/meetings/{id}

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 ​

DELETEhttps://api.voicelab.uz/v1/meetings/{id}

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 ​

POSThttps://api.voicelab.uz/v1/meetings/{id}/notes/generations

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 statusCodeMeaning
400invalid_idempotency_keyMissing or non-UUID Idempotency-Key
400invalid_multipartMalformed form, missing audio, or unknown/repeated parts
400invalid_meeting_queryInvalid limit, cursor, or ids
401invalid_api_keyMissing, invalid, expired, disabled or revoked API key
402insufficient_creditsAccount balance cannot cover the reservation
402api_key_credit_limitRequest would exceed the key's credit limit
403insufficient_scopeKey lacks meetings:read or meetings:write
403workspace_product_unavailableKey belongs to a team workspace
403workspace_mismatch, workspace_forbiddenWorkspace header or membership does not match the key
404meeting_not_foundUnknown, deleted, or another account's meeting
409idempotency_key_reusedSame key sent with different content
409meeting_processingThe original request with this key is still in progress
409meeting_notes_not_retryableNotes regeneration requested for a meeting that is not transcribed
409meeting_not_readyOperation requires completed processing
413audio_too_largeUpload exceeds 500 MiB
415unsupported_audio_formatFile contents are not a supported audio format
422invalid_audioUnreadable audio, or duration outside 0.5 seconds–24 hours
422invalid_title, invalid_language, validation_errorInvalid title, language, or rename body
429rate_limitedShared per-account request budget exceeded
429meeting_queue_fullTen meetings are already queued or running
429meeting_upload_capacityUpload capacity is temporarily busy
503meeting_billing_unavailablePricing unavailable; nothing was queued
503meeting_feature_unavailable, meeting_worker_unavailableMeeting AI or its processor is unavailable; nothing was queued
503developer_auth_unavailableAPI key verification is temporarily unavailable
504meeting_request_timeoutRequest timed out; check status before retrying
500meeting_internal_errorUnexpected 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.