Skip to content

Import Call Recordings and Transcripts

The Partner API accepts a single multipart upload that contains either a text transcript or an audio recording and attaches the result to a Contio meeting. The API decides which pipeline to run from the uploaded file's extension:

  • Text files (.txt, .md, .srt, .vtt, .pdf) are parsed synchronously and return HTTP 200 with the created transcript.
  • Audio files are transcribed asynchronously and return HTTP 202 with a job you poll for completion.

This guide covers the multipart contract, bound vs. unbound imports, polling audio jobs, error handling, limits, and SDK usage.

Recording consent

Contio does not verify that participants consented to being recorded. The partner application that uploads a recording is responsible for obtaining and retaining any consent required by applicable law before importing it.

Endpoints

Operation Method and path Scope Success
Import into an existing meeting POST /v1/partner/user/meetings/{id}/transcript/import meetings:write 200 (text) / 202 (audio)
Import and create a meeting POST /v1/partner/user/transcript-imports meetings:write 200 (text) / 202 (audio)
Read audio job status GET /v1/partner/user/transcript-imports/audio/jobs/{jobId} meetings:read or meetings:write 200

All three operations are Partner User API operations and require an OAuth access token. Full request/response schemas are in the OpenAPI Specification under importMeetingTranscript, importTranscript, and getAudioTranscriptImport.

Dispatch flow

flowchart TD
    A[POST multipart/form-data with file] --> B{File extension}
    B -->|.txt .md .srt .vtt .pdf| C[Parse transcript]
    C --> D[200 kind=text<br/>transcript_id, segments_count]
    B -->|.mp3 .m4a .wav ...| E[Plan + credit eligibility]
    E -->|ineligible| F[402 feature_not_available / 402 insufficient_credits]
    E -->|eligible| G[Create audio job]
    G --> H[202 kind=audio<br/>job_id, state=pending]
    H --> I["GET .../audio/jobs/{jobId}"]
    I -->|state != completed/failed| I
    I -->|completed| J[meeting_id, transcript_id]
    I -->|failed| K[failure_code]
    B -->|anything else| L[400 unsupported_format]

Supported file types

Kind Extensions
Text .txt, .md, .srt, .vtt, .pdf
Audio .mp3, .m4a, .mp4, .wav, .aac, .ogg, .oga, .opus, .flac, .webm, .amr, .aiff, .wma

PDF imports use the document's text layer only; scanned or image-only PDFs contain no extractable text and are rejected with 400 invalid_request.

Dispatch is by extension only. A .mp4 or .webm container is accepted as audio; if it contains no audio track the job fails later with video_no_audio. Any other extension is rejected with 400 unsupported_format before anything is stored.

Request format

Both POST endpoints accept multipart/form-data.

Field Type Applies to Description
file file both Required. The transcript or recording. The filename extension selects the pipeline.
meeting_id string /transcript-imports only Optional. When present the import is bound to this existing meeting (equivalent to the /meetings/{id}/transcript/import route). When absent a meeting is created for the import.
title string unbound Optional meeting title.
starts_at RFC 3339 timestamp unbound Optional meeting start time.
duration_seconds integer ≥ 0 unbound Optional meeting duration.
participants string (repeatable) unbound Optional participant names or emails. Repeat the field once per participant.
calendar_event_id string audio, unbound Optional calendar event to bind the created meeting to.

title, starts_at, duration_seconds, and participants are ignored for bound imports because the target meeting already has that metadata.

Bound vs. unbound imports

  • Bound — the meeting already exists in Contio. Use POST /meetings/{id}/transcript/import, or POST /transcript-imports with meeting_id. The meeting must not have started, must not be finalized, and must not already hold a transcript longer than two minutes; otherwise the request returns 409 conflict.
  • Unbound — no meeting exists yet. Use POST /transcript-imports without meeting_id. Contio creates a meeting from the optional metadata (or from calendar_event_id for audio) and attaches the transcript to it. The created meeting_id is returned in the text response, or on the audio job once it reaches completed.

Text imports (synchronous)

Text uploads are parsed in the request and return 200 OK with kind: "text":

{
  "kind": "text",
  "meeting_id": "mtg_01J8...",
  "transcript_id": "rm_01J8...",
  "segments_count": 142,
  "duration_seconds": 1860,
  "started_at": "2026-09-16T14:00:00Z",
  "ended_at": "2026-09-16T14:31:00Z"
}
curl -X POST "https://api.contio.ai/v1/partner/user/meetings/mtg_01J8.../transcript/import" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "file=@call.vtt"

Text imports are not plan-gated and do not consume credits.

Audio imports (asynchronous)

Audio uploads are staged, checked for plan and credit eligibility, and queued. The response is 202 Accepted with kind: "audio":

{
  "kind": "audio",
  "job_id": "aij_01J8...",
  "state": "pending",
  "created_at": "2026-09-16T14:32:10Z",
  "updated_at": "2026-09-16T14:32:10Z"
}
curl -X POST "https://api.contio.ai/v1/partner/user/transcript-imports" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "file=@call.m4a" \
  -F "title=Q3 pipeline review" \
  -F "starts_at=2026-09-16T14:00:00Z" \
  -F "participants=alice@example.com" \
  -F "participants=bob@example.com"

Job states

Poll GET /v1/partner/user/transcript-imports/audio/jobs/{jobId} until the job reaches a terminal state.

State Terminal Meaning
pending no Queued, waiting for a worker.
acquiring no Worker is reading the staged upload.
validating no Probing the container, duration, and size.
transcribing no Speech-to-text in progress.
attaching no Binding the meeting and writing the transcript.
completed yes meeting_id and transcript_id are populated.
failed yes failure_code explains why.
curl "https://api.contio.ai/v1/partner/user/transcript-imports/audio/jobs/aij_01J8..." \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "kind": "audio",
  "job_id": "aij_01J8...",
  "state": "completed",
  "meeting_id": "mtg_01J8...",
  "transcript_id": "rm_01J8...",
  "created_at": "2026-09-16T14:32:10Z",
  "updated_at": "2026-09-16T14:41:52Z",
  "completed_at": "2026-09-16T14:41:52Z"
}

Transcription time scales with recording length. Poll every 5–15 seconds and apply an overall timeout on your side; a one-hour recording can take several minutes. Jobs that exceed the server-side wall-clock limit are failed with timeout.

Failure codes

When state is failed, failure_code is one of the values below.

Non-exhaustive

This table lists the codes the import pipeline emits today. New codes may be added without a version bump; treat unknown values as a generic failure and surface the code to your support tooling.

failure_code Stage Meaning / what to do
unsupported_format validating Container or codec is not supported. Re-encode (for example to MP3 or M4A) and resubmit.
container_mismatch validating File contents do not match the extension. Fix the filename or re-export the file.
video_no_audio validating A video container with no audio track.
empty_or_corrupt validating The file is empty or could not be decoded.
zero_duration validating Decodable but contains no audio samples.
too_large validating Exceeds the size limit (see Limits).
too_long validating Exceeds the duration limit (see Limits).
source_not_found acquiring The staged upload expired or was removed before a worker picked it up. Resubmit.
acquire_failed acquiring Could not read the staged upload. Retry later.
provider_access_denied acquiring Storage provider rejected the read. Not retryable by the partner; contact support.
invalid_input acquiring / attaching The job was submitted with inconsistent metadata. Check the form fields and resubmit.
stt_unavailable transcribing Speech-to-text provider unavailable. Retry later.
stt_session_invalid transcribing The transcription session was rejected by the provider. Retry later.
transcription_failed transcribing Transcription produced no usable output.
timeout any The job exceeded the server-side wall-clock limit.
bind_failed attaching An unbound import could not be bound to or create a meeting (for example an invalid calendar_event_id).
eligibility_error attaching Eligibility could not be re-evaluated when attaching. Retry later.
conflict_meeting_already_started, conflict_meeting_finalized, conflict_transcript_exists, conflict_plan_required, conflict_no_credits attaching The meeting became ineligible between submission and attach (someone started it, it was finalized, another transcript landed, or the plan/credits changed).
conflict_late_eligibility attaching The transcript could not be attached because the meeting's eligibility changed at the last moment.
attach_failed attaching Writing the transcript failed. Retry later.
internal_error any Unexpected error. Retry later; contact support with the job_id if it persists.

Errors

Except for 402, errors use the standard PartnerErrorResponse envelope (error, code, request_id).

Status code When
400 invalid_request Missing file, malformed starts_at, negative duration_seconds, an invalid meeting_id, or a file that could not be parsed (e.g. an image-only PDF with no text layer).
400 unsupported_format Extension is neither a text nor an audio type.
401 unauthorized Missing or expired access token.
402 feature_not_available Audio only. The workspace plan does not include AUDIO_IMPORT (Elite). Body is the FeatureNotAvailableResponse envelope with feature, required_plan, and current_plan.
402 insufficient_credits Audio only. The workspace has no remaining credits.
403 forbidden Token lacks meetings:write.
404 not_found The meeting or job does not exist or is not accessible to the token's user, or transcript import is not enabled for the workspace.
409 conflict The meeting has already started, is finalized, or already has a transcript longer than two minutes.
413 request_too_large The audio upload exceeds the audio size limit, or a text/PDF upload exceeds the transcript size or parsed-output limits.
500 internal_error Unexpected server error.

Two 402 shapes

A 402 from an audio upload is either the plan gate (code: "feature_not_available", FeatureNotAvailableResponse envelope) or an empty credit balance (code: "insufficient_credits", standard PartnerErrorResponse). Branch on code, not on the status alone.

{
  "code": "feature_not_available",
  "message": "Audio transcript import is not included in the workspace plan",
  "feature": "AUDIO_IMPORT",
  "required_plan": "ELITE",
  "current_plan": "PRO"
}

Retries and idempotency

Multipart endpoints are excluded from the Idempotency-Key mechanism described in Idempotency. Retrying a transcript import is not deduplicated by the server:

  • Retrying a text import into the same meeting returns 409 conflict once the first attempt has stored a transcript longer than two minutes.
  • Retrying an audio import creates a new job each time. If the first job is still running, both jobs proceed and the second one fails at the attach stage with conflict_transcript_exists or conflict_late_eligibility; if the first job already completed, the second submission returns 409 conflict.

Recommended retry pattern:

  1. Submit the upload once and persist the returned job_id (audio) or transcript_id (text) alongside your own record.
  2. If the response was lost (timeout, connection reset), do not blindly resubmit. For audio, call getAudioTranscriptImport with the stored job_id; for either kind, check whether the meeting already has a transcript before uploading again.
  3. Treat 409 conflict on a retry as success of the earlier attempt, not as an error.

Limits

Limit Default
Maximum audio upload size 2 GiB (2147483648 bytes) — enforced as 413 request_too_large on upload and too_large during validation
Maximum audio duration 3 hours (10800 seconds) — enforced as too_long during validation
Maximum text/PDF transcript upload size 50 MiB (52428800 bytes, TRANSCRIPT_IMPORT_MAX_BYTES) — enforced as 413 request_too_large
Maximum PDF pages 500 pages — enforced as 413 request_too_large
Maximum parsed transcript output 50,000 segments / 10 MiB of transcript text — enforced as 413 request_too_large

Limits are configured per environment and may differ for your partner agreement.

Plan gating and rollout

  • Text import is available on every plan.
  • Audio import requires the AUDIO_IMPORT feature, which is included from the Elite plan tier. Lower tiers receive 402 feature_not_available with the FeatureNotAvailableResponse envelope at submission time. Audio transcription also consumes workspace credits; a workspace with no balance receives 402 insufficient_credits.
  • Transcript import is rolled out per workspace behind a feature flag. Where the flag is disabled the endpoints respond 404 not_found. Contact your partner manager if a workspace you expect to be enabled returns 404.

SDK usage

Requires @contio/partner-sdk 1.12.0

The methods below ship in SDK 1.12.0. Earlier versions can still call the endpoints through the generic transport, but the typed helpers and waitForAudioTranscriptImport are not available.

import { readFile } from 'node:fs/promises';
import { ContioPartnerSDK, ContioAPIError } from '@contio/partner-sdk';

const sdk = ContioPartnerSDK.forUser({ clientId, clientSecret, redirectUri });
// ...complete the OAuth flow and store tokens...
const user = sdk.user;

// Text: synchronous 200
const vtt = new Blob([await readFile('call.vtt')], { type: 'text/vtt' });
const text = await user.importMeetingTranscript('mtg_01J8...', {
  file: vtt,
  filename: 'call.vtt',
});
if (text.kind === 'text') {
  console.log(`Imported ${text.segments_count} segments into ${text.meeting_id}`);
}

// Audio: 202 + poll
const audio = new Blob([await readFile('call.m4a')], { type: 'audio/mp4' });
const accepted = await user.importTranscript({
  file: audio,
  filename: 'call.m4a',
  title: 'Q3 pipeline review',
  starts_at: '2026-09-16T14:00:00Z',
  participants: ['alice@example.com', 'bob@example.com'],
});

if (accepted.kind === 'audio') {
  try {
    const job = await user.waitForAudioTranscriptImport(accepted.job_id, {
      intervalMs: 10_000,
      timeoutMs: 20 * 60_000,
    });
    if (job.state === 'completed') {
      console.log(`Transcript ${job.transcript_id} attached to ${job.meeting_id}`);
    } else {
      console.error(`Import failed: ${job.failure_code}`);
    }
  } catch (err) {
    if (err instanceof ContioAPIError) {
      console.error(err.statusCode, err.code, err.message);
    } else {
      throw err; // polling timed out; the job may still complete server-side
    }
  }
}

waitForAudioTranscriptImport polls getAudioTranscriptImport until the job is completed or failed, and rejects with an error when timeoutMs elapses first. A timeout does not cancel the job — store the job_id and check it again later.

See SDK Examples for authentication setup and the Agent Integration guide for the equivalent MCP tool definitions (importMeetingTranscript, importTranscript, getAudioTranscriptImport).

Next steps

  • Idempotency — why multipart uploads are excluded and how to retry safely
  • Feature Gating — plan-gated endpoints and the 402 envelope
  • Webhook Events — react to meeting and transcript lifecycle events instead of polling
  • Release Notes — what shipped in v1.12