Skip to content

API

Meeting Summary API

Upload meeting audio or video, create an asynchronous job, and poll for speaker transcripts, translations, and a structured summary. Supports WhisperX + pyannote and ElevenLabs Scribe.

Use a key from Dashboard → API Keys as x-api-key: kvid_… or Authorization: Bearer kvid_…. No browser login is required. Jobs and credits belong to the key creator on the current site. Keys created by the same user can access that user's dashboard recordings on that site; other users' recordings remain private.

Upload and create a job

First upload a file using multipart field file. Then send the returned uploadReceipt in a JSON job request. Never send a local file path or a media URL in place of the receipt.

POST /uploads

curl --fail-with-body "https://kaleidovid.com/api/conference-summary/v1/uploads" \
  -H "x-api-key: $KALEIDOVID_API_KEY" \
  -F "file=@meeting.mp3"

POST /jobs

curl --fail-with-body "https://kaleidovid.com/api/conference-summary/v1/jobs" \
  -H "x-api-key: $KALEIDOVID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uploadReceipt":"REPLACE_WITH_UPLOAD_RECEIPT","sourceLanguage":"auto","targetLanguage":"zh-cn","asrEngine":"whisperx_asr","noiseReduction":true}'

# HTTP 201: {"id":"cm_meeting_job"}

Submitting the same valid receipt again returns the existing job ID (HTTP 200). A new upload creates a new recording. Submission is asynchronous; always poll even after HTTP 201, as dispatch or processing can fail.

Request fields and limits

FieldTypeDescription
uploadReceiptstringRequired. Signed receipt returned by the upload endpoint; expires after 2 hours.
sourceLanguagestringRequired. auto or a language code below.
targetLanguagestringRequired. A language code below; auto is not supported.
asrEnginestringwhisperx_asr (default) | elevenlabs_scribe
noiseReductionbooleanDefault true. Extract vocals and reduce background noise before transcription.
minSpeakers / maxSpeakersinteger0–30; default 0 (automatic). When maxSpeakers is nonzero, minSpeakers must not exceed it.

Language codes: zh-cn, zh-tw, en, es, fr, de, ja, ko, pt, ru, ar, it, th, vi, id, tr, pl, nl

Formats: MP4, MOV, AVI, MKV, MP3, M4A, WAV. Maximum recording length: 2 hours. GET /jobs returns the effective maxUploadBytes and current pricing. Cost is the greater of the configured minimum and 4 credits × each started minute. Credits are charged once upon successful completion. Retranslating or regenerating a completed summary does not charge that recording again.

The API key dashboard's realtime audio and TTS concurrency quotas do not apply to meeting summaries. This API uses the recording duration, upload, feature-access, and credit checks described here.

Results and job controls

GET /jobs/{id}

curl --fail-with-body "https://kaleidovid.com/api/conference-summary/v1/jobs/cm_meeting_job" \
  -H "Authorization: Bearer $KALEIDOVID_API_KEY"

The response is {job: {...}}. job.status is COMPLETED, FAILED, or CANCELLED when processing stops. Other states include PENDING, EXTRACTING_AUDIO, CLEANING_AUDIO, TRANSCRIBING, TRANSCRIPT_READY, TRANSLATING, SUMMARIZING, and DELETING. job.progress is 0–100; check warning and errorMessage for partial results or failures. Poll every 5 seconds.

Completed result (excerpt)

{
  "job": {
    "id": "cm_meeting_job",
    "status": "COMPLETED",
    "progress": 100,
    "credits": 20,
    "creditsCharged": true,
    "transcript": [
      {
        "id": "segment_1",
        "start": 0,
        "end": 4.2,
        "speakerId": "speaker_1",
        "text": "We will send the proposal tomorrow."
      }
    ],
    "translatedTranscript": [
      {
        "id": "segment_1",
        "start": 0,
        "end": 4.2,
        "speakerId": "speaker_1",
        "text": "我们明天会发送方案。"
      }
    ],
    "speakerNames": {
      "speaker_1": "Alice"
    },
    "summary": {
      "overview": "讨论了方案发送时间。",
      "topics": [],
      "decisions": [],
      "actionItems": [
        {
          "text": "发送方案",
          "segmentIds": [
            "segment_1"
          ],
          "ownerSpeakerId": "speaker_1",
          "dueDate": "tomorrow"
        }
      ],
      "openQuestions": []
    },
    "warning": null,
    "errorMessage": null
  }
}

Transcript timestamps are seconds. Summary topics, decisions, actionItems, and openQuestions reference transcript segmentIds. A summary may also contain briefing with columns, priority rows, and followUp. Fetch GET /jobs for the newest 100 recording headers, upload limits, and pricing.

  • POST /jobs/{id} — {"action":"cancel"}, {"action":"retry"}, {"action":"resummarize"}, {"action":"retranslate","targetLanguage":"en"}. Returns {success:true}; poll for the result. Retry requires FAILED/CANCELLED; resummarize/retranslate require stopped processing and a valid transcript.
  • PATCH /jobs/{id} — {"title":"Weekly meeting","speakerNames":{"speaker_1":"Alice"}}. Returns {job:{...}}. Speaker IDs must exist in the transcript.
  • DELETE /jobs/{id} — Delete the recording and files after completion, failure, or cancellation. Returns {success:true}.

Errors use {"error":"message"}: 400 invalid input/receipt, 401 missing/invalid/revoked/expired key, 402 insufficient credits, 403 feature unavailable, 404 inaccessible recording, 409 conflicting job state, 413 upload/duration limit, 500 processing service error, 503 authentication or cleanup unavailable.

Python example

Install requests and set KALEIDOVID_API_KEY in your server environment, then run:

Python · upload, create, poll

import os, time, requests

base = "https://kaleidovid.com/api/conference-summary/v1"
headers = {"x-api-key": os.environ["KALEIDOVID_API_KEY"]}

with open("meeting.mp3", "rb") as recording:
    upload = requests.post(base + "/uploads", headers=headers,
        files={"file": ("meeting.mp3", recording, "audio/mpeg")}, timeout=3600)
upload.raise_for_status()

created = requests.post(base + "/jobs", headers=headers, json={
    "uploadReceipt": upload.json()["uploadReceipt"],
    "sourceLanguage": "auto", "targetLanguage": "zh-cn",
    "asrEngine": "whisperx_asr", "noiseReduction": True,
    "minSpeakers": 0, "maxSpeakers": 0
}, timeout=60)
created.raise_for_status()
job_id = created.json()["id"]

# Persist job_id so polling can resume without submitting another recording.
deadline = time.monotonic() + 4 * 60 * 60
while time.monotonic() < deadline:
    response = requests.get(base + "/jobs/" + job_id, headers=headers, timeout=30)
    response.raise_for_status()
    job = response.json()["job"]
    if job["status"] == "COMPLETED":
        print(job["transcript"])
        print(job["translatedTranscript"])
        print(job["summary"])
        break
    if job["status"] in ("FAILED", "CANCELLED"):
        raise RuntimeError(job.get("errorMessage") or job["status"])
    time.sleep(5)
else:
    raise TimeoutError("Polling timed out; resume with job ID " + job_id)