Help center

Help center

A short guide for first upload and exports.

For developers

External API reference

REST API reference for integrating with Vibe2Text using a personal key (v2t_…). Every example is a copy-pasteable curl command: upload files, run transcription, generate reports and exports, import from a link, record calls, and subscribe to webhooks.

Requests and responses are JSON over HTTPS, and every path is under /api/v1. A key unlocks the methods listed below; deleting, manual editing, chat, search, and billing are available only from the web app. The base URL is https://api.vibe2text.ru

Getting started: key & auth

Every request is authorised with a single personal API key shaped like v2t_… Create it once and pass it as a header on every call.

  • Create a key. Keys are created only from a signed-in web session: Settings → API keys → Create, giving it a name and the scopes it needs. The full v2t_… value is shown once — copy it immediately. You can keep up to 5 active keys; list them with GET /api/v1/user/api-keys/ and revoke with DELETE /api/v1/user/api-keys/:id (session only, not via a key).
  • Send the key. Add the header `Authorization: Bearer v2t_YOUR_TOKEN` to each request.
  • Scopes. A key carries a set of scopes. Calling an endpoint that keys can't reach returns 403 asking you to sign in via a session; missing the right scope returns 403 "API key requires scope X". The `*` scope grants every scope in the table below.
  • Use HTTPS. Always call us over HTTPS. Sending keys over plain HTTP is not safe.

Request

# Create a key from a signed-in web session:
#   Settings → API keys → Create (give it a name + the scopes it needs).
# The full v2t_... value is shown ONCE — copy it immediately.
# Up to 5 active keys; revoke from the same screen (session only).

# Pass the key on every request via the Authorization header.
curl https://api.vibe2text.ru/api/v1/transcripts/?limit=2 \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN'

Response

{
  "items": [
    {
      "id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
      "organization_id": null,
      "file_id": "7040f5b9-2492-4934-b284-f5eb715f6e0d",
      "title": "voice.ogg",
      "language": "",
      "detected_language": null,
      "status": "completed",
      "progress": 1.0,
      "error": null,
      "error_code": null,
      "error_hint": null,
      "duration_s": 3.44,
      "word_count": 10,
      "speaker_count": 1,
      "smart_reports_count": 2,
      "credits_charged": 0,
      "created_at": "2026-06-04T00:17:34Z",
      "completed_at": "2026-06-04T00:17:50Z",
      "shared": false,
      "share_role": null,
      "owner_name": null
    }
  ],
  "meta": { "total": 24, "skip": 0, "limit": 2 }
}
Scopes

16 values grouped into 8 families. Most resources have a read (:read) and a write (:write) scope. Grant a key only what it needs.

FamilyReadWritePurpose
Transcriptstranscripts:readtranscripts:writeStart transcription and read text, segments, speakers.
Filesfiles:readfiles:writeMultipart upload of recordings and upload status.
Source importsimports:readimports:writeImport audio/video from a URL (YouTube, podcasts, etc.).
Exportsexports:readexports:writeExport transcripts to docx, pdf, srt and other formats.
Smart reportsreports:readreports:writeCreate and read AI reports on transcripts.
Knowledge baseskb:readkb:writeManage knowledge bases and add entries.
Webhookswebhooks:readwebhooks:writeSubscribe to events and inspect deliveries.
Recording jobsrecordings:readrecordings:writeSend a bot to record a meeting and check job status.

The `*` scope is equivalent to granting every scope above at once.

The core flow: upload → transcribe → fetch

The base path from a file to finished text is six steps. Each step is tagged with the scope it needs.

  1. 1. Start a multipart upload and get a file_id plus the list of parts.

    Request

    # 1) Start a multipart upload (scope files:write; rate-limit 10/h).
    #    Optionally send an Idempotency-Key (<=128 chars) — reusing it within
    #    60s returns the same upload instead of starting a new one.
    curl -X POST https://api.vibe2text.ru/api/v1/files/upload-init \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -H 'Idempotency-Key: my-upload-2026-06-01' \
      -d '{
        "filename": "interview.mp3",
        "size_bytes": 25400000,
        "mime_type": "audio/mpeg"
      }'

    Response

    {
      "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
      "upload_id": "1ea0eeef1f77d8dc",
      "s3_key": "files/<user_id>/75ad1a37-8945-4613-afb2-5748f7a47cd1.mp3",
      "upload_kind": "multipart_s3",
      "chunk_size": 8388608,
      "parts": [
        {
          "part_no": 1,
          "url": "https://uploads.vibe2text.ru/<pre-signed-put-url>",
          "expires_at": "2026-06-04T01:41:17Z"
        }
      ],
      "expires_at": "2026-06-04T01:41:17Z",
      "presigned_put_url": "https://uploads.vibe2text.ru/<pre-signed-put-url>"
    }
  2. 2. PUT each chunk straight to the pre-signed upload URL and capture every part's ETag.

    Request

    # 2) PUT each chunk straight to the pre-signed upload url and capture its ETag.
    #    Pre-signed urls expire after 3600s — re-presign an expired part with
    #    POST /files/upload-init/{file_id}/parts/{part_no}/presign.
    #    GET /files/{file_id}/upload-status lists which parts still need bytes.
    curl -X PUT "<parts[0].url>" \
      --upload-file ./chunk-1.bin \
      -D - -o /dev/null
    # Grab the ETag header from each response.

    Response

    HTTP/1.1 200 OK
    ETag: "9b2cf535f27731c974343645a3985328"
    # Save the ETag of each part for upload-complete.
  3. 3. Finalise the upload with every part's ETag.

    Request

    # 3) Finalise the upload with every part's ETag (scope files:write).
    curl -X POST https://api.vibe2text.ru/api/v1/files/upload-complete \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -d '{
        "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
        "parts": [ { "part_no": 1, "etag": "\"9b2cf5...\"" } ]
      }'

    Response

    {
      "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
      "s3_key": "files/<user_id>/75ad1a37...mp3",
      "size": 2048000,
      "status": "ready"
    }
  4. 4. Start transcription on the file_id.

    Request

    # 4) Start transcription (scope transcripts:write). file_id is required.
    #    Omit "language" to auto-detect; "diarize" defaults to true.
    #    Set "auto_smart_report": true to also queue a summary automatically.
    curl -X POST https://api.vibe2text.ru/api/v1/transcripts/ \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -d '{ "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1", "title": "Interview", "diarize": true }'

    Response

    {
      "id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
      "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
      "title": "Interview",
      "status": "pending",
      "progress": 0.0,
      "created_at": "2026-06-04T00:17:34Z"
    }
  5. 5. Poll status until it is completed.

    Request

    # 5) Poll status until it is "completed" (scope transcripts:read).
    curl https://api.vibe2text.ru/api/v1/transcripts/{id}/status \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN'

    Response

    {
      "id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
      "status": "completed",
      "progress_percent": 100
    }
  6. 6. Read the result: the full object, or segments plus the speaker list.

    Request — /segments

    # 6) Read the result (scope transcripts:read). Either pull the full
    #    payload, page through segments, or read the speaker list.
    curl "https://api.vibe2text.ru/api/v1/transcripts/{id}/segments?limit=2&include_words=true" \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN'

    Response

    {
      "items": [
        {
          "id": "41d7346e-6d54-4551-b7e7-73d320740254",
          "seq": 1,
          "start_s": 0.48,
          "end_s": 3.44,
          "speaker_id": "3baad45e-3631-48bb-927d-5abff2351029",
          "text": "Один, два, три, четыре, пять, шесть, семь, восемь, девять, десять.",
          "confidence": 1.0,
          "edited": false,
          "words": [
            { "start_s": 0.48, "end_s": 0.88, "text": "Один,", "confidence": null },
            { "start_s": 0.96, "end_s": 1.2, "text": "два,", "confidence": null }
          ],
          "language": "auto"
        }
      ],
      "total": 1,
      "offset": 0,
      "limit": 2
    }

    Request — /speakers

    curl https://api.vibe2text.ru/api/v1/transcripts/{id}/speakers \
      -H 'Authorization: Bearer v2t_YOUR_TOKEN'

    Response

    [
      {
        "id": "3baad45e-3631-48bb-927d-5abff2351029",
        "label": "S1",
        "name": "Спикер 1",
        "avatar_color": "#4F46E5",
        "speaking_seconds": 0
      }
    ]

Alternate ingestion: instead of steps 1–4 you can POST a URL to /source-imports/ or start a meeting recording via POST /recording-jobs/ — then poll the job and read the transcript_id it produces, exactly like step 6.

Per-resource reference

The full list of endpoints a key can reach, with method, path, required scope and purpose. All paths are relative to the /api/v1 prefix.

Files

files:write / files:read

Multipart upload of recordings. upload-init returns file_id, upload_id, chunk_size and a list of parts (part_no + url + expires_at).

  • POST/files/upload-init— request an upload (filename, size_bytes, mime_type?, sha256?); rate-limit 10/h; Idempotency-Key for 60s
  • POST/files/upload-init/:file_id/parts/:part_no/presign— re-presign an expired part
  • POST/files/upload-complete— finalise the upload (file_id, parts (part_no + etag))
  • GET/files/:id— file metadata
  • GET/files/:id/download-url— download link
  • GET/files/:id/upload-status— which parts still need bytes

Example response

# POST /files/upload-init →
{
  "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
  "upload_id": "1ea0eeef1f77d8dc",
  "s3_key": "files/<user_id>/75ad1a37-...-f5eb715f6e0d.mp3",
  "upload_kind": "multipart_s3",
  "chunk_size": 8388608,
  "parts": [
    { "part_no": 1, "url": "https://uploads.vibe2text.ru/<pre-signed-put-url>", "expires_at": "2026-06-04T01:41:17Z" }
  ],
  "expires_at": "2026-06-04T01:41:17Z",
  "presigned_put_url": "https://uploads.vibe2text.ru/<pre-signed-put-url>"
}

# POST /files/upload-complete →
{ "file_id": "75ad1a37-...", "s3_key": "files/<user_id>/75ad1a37...mp3", "size": 2048000, "status": "ready" }

Transcripts — read

transcripts:read

Read transcripts and related data. ASR model ids are not serialized in responses.

  • GET/transcripts/— list with filters (skip, limit≤200, status, q, language, date_from/to, speaker, tag, smart, sort)
  • GET/transcripts/:id— full object + segments + speakers
  • GET/transcripts/:id/meta— metadata only
  • GET/transcripts/:id/status— poll status
  • GET/transcripts/:id/segments— segments (offset, limit≤2000, include_words)
  • GET/transcripts/:id/speakers— speakers
  • GET/transcripts/:id/chapters— chapters
  • GET/transcripts/:id/action-items— action items
  • GET/transcripts/:id/annotations— annotations
  • GET/transcripts/:id/content-events— content events
  • GET/transcripts/:id/scope-config— scope configuration
  • GET/transcripts/:id/suggestions— suggestions

Example response

# GET /transcripts/?limit=2 →
{
  "items": [
    {
      "id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
      "file_id": "7040f5b9-2492-4934-b284-f5eb715f6e0d",
      "title": "voice.ogg",
      "status": "completed",
      "progress": 1.0,
      "duration_s": 3.44,
      "word_count": 10,
      "speaker_count": 1,
      "smart_reports_count": 2,
      "created_at": "2026-06-04T00:17:34Z",
      "completed_at": "2026-06-04T00:17:50Z",
      "shared": false
    }
  ],
  "meta": { "total": 24, "skip": 0, "limit": 2 }
}

# GET /transcripts/{id}/meta → (same item fields) +
"file": {
  "id": "7040f5b9-2492-4934-b284-f5eb715f6e0d",
  "name": "voice.ogg",
  "mime": "audio/mp4",
  "size": 13041,
  "public_url": "https://media.vibe2text.ru/<pre-signed-stream-url>"
}

# GET /transcripts/{id}/status →
{ "id": "9f3e4d3a-...", "status": "completed", "progress_percent": 100 }

# GET /transcripts/{id}/speakers →
[ { "id": "3baad45e-...", "label": "S1", "name": "Спикер 1", "avatar_color": "#4F46E5", "speaking_seconds": 0 } ]

Transcripts — write

transcripts:write

Create a transcript and launch AI processing only (manual text edits are not available via a key).

  • POST/transcripts/— create (file_id required; title?, language?, diarize=true, auto_smart_report=false, clip_start_s?, clip_end_s?)
  • POST/transcripts/:id/detect-speakers— detect speakers
  • POST/transcripts/:id/generate-chapters— generate chapters
  • POST/transcripts/:id/speakers/auto-name— auto-name speakers
  • POST/transcripts/:id/chapters/regenerate— regenerate chapters
  • POST/transcripts/:id/action-items/extract— extract action items

Example response

# POST /transcripts/ →
{
  "id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
  "file_id": "75ad1a37-8945-4613-afb2-5748f7a47cd1",
  "title": "Interview",
  "status": "pending",
  "progress": 0.0,
  "created_at": "2026-06-04T00:17:34Z"
}

Exports

exports:read / exports:write

Export transcripts to a file. Formats: docx, pdf, srt, vtt, txt, json, md, xlsx.

  • POST/exports/— queue an export (transcript_id, format)
  • GET/exports/— list (?transcript_id)
  • GET/exports/:id— poll status
  • GET/exports/:id/download— link (url, expires_in)
  • POST/transcripts/:id/exports— nested export launch
  • GET/transcripts/:id/exports— nested export list

Example response

# POST /exports/ →
{
  "id": "b0d7ef8b-1f21-4448-8b50-29e3d06df0b8",
  "transcript_id": "9f3e4d3a-...",
  "format": "pdf",
  "status": "pending",
  "size_bytes": null,
  "error": null,
  "expires_at": "2026-06-11T00:41:18Z",
  "created_at": "2026-06-04T00:41:18Z"
}

# GET /exports/{id} → (status becomes "ready", size_bytes filled)
{ "id": "b0d7ef8b-...", "status": "ready", "size_bytes": 25603, "format": "pdf", ... }

# GET /exports/{id}/download →
{ "url": "https://media.vibe2text.ru/<pre-signed-download-url>", "expires_in": 3600 }

Source imports

imports:read / imports:write

Import audio/video from a public URL. Once ready the response carries a transcript_id.

  • POST/source-imports/— start an import (url, kind?, title?, language?, clip_*?); rate-limit 30/h
  • POST/source-imports/preview— preview a link
  • POST/source-imports/:id/retry— retry an import
  • GET/source-imports/— list imports
  • GET/source-imports/providers— supported sources
  • GET/source-imports/:id— import status

Example response

# GET /source-imports/providers →
{
  "providers": [
    { "code": "direct_url", "label": "Direct URL", "status": "available" },
    { "code": "mts_link", "label": "MTS Link", "status": "available" },
    { "code": "rutube", "label": "RuTube", "status": "available" },
    { "code": "vk_video", "label": "VK Video", "status": "available" },
    { "code": "twitch", "label": "Twitch (VODs)", "status": "available" },
    { "code": "dzen", "label": "Dzen Video", "status": "available" }
  ]
}

# POST /source-imports/preview →
{ "title": null, "thumbnail": null, "duration_s": null, "provider": "rutube", "supported": true }

Smart reports

reports:read / reports:write

Create and read reports only. Chat, regenerate, export, streaming and revisions are not available via a key. Up to 3 concurrent LLM jobs per user (else 429).

  • POST/smart-reports/— create (transcript_id OR transcript_ids[]; type required; model?, style_hint?, prompt_template_id?, render_mode=auto, knowledge_base_ids?, instructions?; Idempotency-Key)
  • GET/smart-reports/— list reports
  • GET/smart-reports/by-transcript/:id— reports for a transcript
  • GET/smart-reports/:id— report (status, content_md, content_blocks)

Example response

# POST /smart-reports/ →
{
  "id": "59d0af3f-926a-435b-adc4-b1b766bb87bc",
  "transcript_id": "9f3e4d3a-...",
  "transcript_ids": ["9f3e4d3a-..."],
  "type": "summary",
  "status": "pending",
  "render_mode": "constructor",
  "content_md": null,
  "created_at": "2026-06-04T00:41:00Z"
}

# GET /smart-reports/{id} →
{
  "id": "c7d7a0d3-...",
  "type": "summary",
  "status": "completed",
  "render_mode": "text",
  "content_md": "## Счёт от одного до десяти\n\nВ записи звучит последовательный счёт от 1 до 10.",
  "content_blocks": null,
  "tokens_input": 420,
  "tokens_output": 38,
  "completed_at": "2026-06-04T00:17Z"
}

# GET /smart-reports/ →
{ "items": [ { ...SmartReport... } ], "meta": { "total": 6, "skip": 0, "limit": 20 } }

Knowledge bases

kb:read / kb:write

Manage bases and add entries. Every request requires a scope parameter: append ?personal=true (personal workspace) or ?organization_id=<uuid>. Without it the call returns 400. Semantic search (/query) and deletes are not available via a key.

  • GET/knowledge-bases— list bases
  • GET/knowledge-bases/:id— a base
  • GET/knowledge-bases/:id/entries— base entries
  • POST/knowledge-bases— create a base (name, description?)
  • PATCH/knowledge-bases/:id— edit a base
  • POST/knowledge-bases/:id/entries— add an entry (source_kind, title?, content)
  • POST/knowledge-bases/:id/entries/upload— add an entry from PDF/DOCX

Example response

# POST /ai-processing/knowledge-bases?personal=true →
{
  "id": "62106610-74ef-4a20-964f-a570057bdcaf",
  "name": "Glossary",
  "description": "Terms",
  "scope_user_id": "<user_id>",
  "scope_organization_id": null,
  "entries_count": 0,
  "created_at": "2026-06-04T00:42:08Z",
  "updated_at": "2026-06-04T00:42:08Z"
}

# POST /ai-processing/knowledge-bases/{kb_id}/entries?personal=true →
{
  "entries": [
    {
      "id": "9196354b-...",
      "knowledge_base_id": "62106610-...",
      "source_kind": "text",
      "source_doc_id": "92f53be1-...",
      "title": "ASR",
      "content": "ASR = automatic speech recognition.",
      "tokens_count": 5,
      "chunk_index": 0,
      "created_at": "2026-06-04T00:42:26Z"
    }
  ]
}

# Without a scope param → 400:
{ "detail": [ { "key": "invalid_input_data", "text": "Scope is required: pass organization_id or personal=true." } ] }

Webhooks

webhooks:read / webhooks:write

Subscribe to events. There is no delete — disable a webhook via PATCH. The secret is shown once.

  • POST/webhooks/— create (url, events?; secret once)
  • PATCH/webhooks/:id— edit (can disable)
  • POST/webhooks/:id/retest— test the endpoint
  • POST/webhooks/:id/regenerate-secret— rotate the secret
  • GET/webhooks/— list webhooks
  • GET/webhooks/:id/logs— delivery log

Example response

# POST /webhooks/ →
{
  "id": "0550f52d-...",
  "url": "https://example.com/hook",
  "events": ["transcript.completed"],
  "secret_hint": "p4-X...zkvI",
  "is_active": true,
  "last_delivery_at": null,
  "last_status_code": null,
  "created_at": "2026-06-04T00:41:21Z",
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxx"
}
# "secret" is returned ONLY on create and regenerate-secret. Later reads show "secret_hint".

Recording jobs

recordings:read / recordings:write

A bot joins a meeting and records it; once done the job links to a transcript_id.

  • POST/recording-jobs/— start a recording (platform, meet_url)
  • POST/recording-jobs/:id/cancel— cancel a recording
  • GET/recording-jobs/— list jobs
  • GET/recording-jobs/platforms— supported platforms
  • GET/recording-jobs/:id— job status

Example response

# GET /recording-jobs/platforms →
{
  "platforms": [
    { "code": "yandex_telemost", "label": "Yandex Telemost", "status": "available", "message": null },
    { "code": "mtslink", "label": "MTS Link", "status": "available", "message": null },
    { "code": "sberjazz", "label": "SberJazz", "status": "available", "message": null },
    { "code": "ktalk", "label": "Kontur Talk", "status": "available", "message": null }
  ]
}
Common scenarios
  • Transcribe a link

    Send a URL (e.g. a YouTube video) via a source-import — we fetch and transcribe it, and you pick up the resulting transcript_id.

  • Generate a summary

    Request a Smart Report of type summary on a ready transcript and pick up the report or its file export.

  • Subscribe to an event

    Create a webhook for transcript.completed and get notified automatically, without polling.

Transcribe a link or a meeting recording

Instead of uploading a file by hand you can send a public URL or have a bot record a meeting. Both return a job to poll; once ready they carry a transcript_id you read like any transcript.

# Ingest a public URL (a video, a podcast, a file link).
# scope imports:write; rate-limit 30/h.
curl -X POST https://api.vibe2text.ru/api/v1/source-imports/ \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "url": "https://rutube.ru/video/...", "language": "ru" }'

# Poll the import; once it finishes it carries a transcript_id you can read
# exactly like step 6 above.
curl https://api.vibe2text.ru/api/v1/source-imports/{id} \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN'

Response

{
  "id": "0d5e8f12-3a4b-4c5d-9e0f-112233445566",
  "url": "https://rutube.ru/video/...",
  "provider": "rutube",
  "status": "queued",
  "transcript_id": null,
  "created_at": "2026-06-04T00:41:00Z"
}
# Send a bot to record a live meeting (scope recordings:write).
# Check GET /recording-jobs/platforms for supported platforms.
curl -X POST https://api.vibe2text.ru/api/v1/recording-jobs/ \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "platform": "yandex_telemost", "meet_url": "https://telemost.yandex.ru/j/..." }'

# Poll the job; when done it links to a transcript_id.
curl https://api.vibe2text.ru/api/v1/recording-jobs/{id} \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN'

Response

{
  "id": "a1b2c3d4-5e6f-4071-8293-aabbccddeeff",
  "platform": "yandex_telemost",
  "meet_url": "https://telemost.yandex.ru/j/...",
  "status": "scheduled",
  "transcript_id": null,
  "created_at": "2026-06-04T00:41:00Z"
}

Generate a report

Request a report on a ready transcript (summary, action items, topics), poll its status, and optionally queue a file export.

# Ask for a report on a ready transcript (scopes reports:write + reports:read).
# "type" is required. Pass transcript_id (one) OR transcript_ids (several).
# An Idempotency-Key dedupes retries; you may have 3 LLM jobs in flight (else 429).
curl -X POST https://api.vibe2text.ru/api/v1/smart-reports/ \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: report-9f3e4d3a' \
  -d '{ "transcript_id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff", "type": "summary" }'

Response

{
  "id": "59d0af3f-926a-435b-adc4-b1b766bb87bc",
  "transcript_id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
  "transcript_ids": ["9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff"],
  "type": "summary",
  "status": "pending",
  "render_mode": "constructor",
  "intent": {
    "requested_type": "summary",
    "render_mode": "auto",
    "style_hint": "summary",
    "instructions": null,
    "model": null,
    "transcript_ids": ["9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff"]
  },
  "plan": null,
  "template_id": null,
  "content_md": null,
  "created_at": "2026-06-04T00:41:00Z"
}
# Poll the report and read its rendered content.
curl https://api.vibe2text.ru/api/v1/smart-reports/{id} \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN'

Response

{
  "id": "c7d7a0d3-...",
  "transcript_id": "9f3e4d3a-...",
  "transcript_ids": ["9f3e4d3a-..."],
  "type": "summary",
  "status": "completed",
  "render_mode": "text",
  "content_md": "## Счёт от одного до десяти\n\nВ записи звучит последовательный счёт от 1 до 10.",
  "content_blocks": null,
  "tokens_input": 420,
  "tokens_output": 38,
  "error": null,
  "created_at": "2026-06-04T00:17Z",
  "completed_at": "2026-06-04T00:17Z"
}
# Need a file? Queue an export (scopes exports:write + exports:read):
curl -X POST https://api.vibe2text.ru/api/v1/exports/ \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "transcript_id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff", "format": "pdf" }'

# Then GET /exports/{id} (poll) and GET /exports/{id}/download for the url.

Response

# POST /exports/ →
{
  "id": "b0d7ef8b-1f21-4448-8b50-29e3d06df0b8",
  "transcript_id": "9f3e4d3a-1b2c-4d5e-8f90-aabbccddeeff",
  "format": "pdf",
  "status": "pending",
  "size_bytes": null,
  "error": null,
  "expires_at": "2026-06-11T00:41:18Z",
  "created_at": "2026-06-04T00:41:18Z"
}

# GET /exports/{id}/download →
{ "url": "https://media.vibe2text.ru/<pre-signed-download-url>", "expires_in": 3600 }

The shape of the final report is described in a separate JSON Schema file — handy if you want to validate the response on your end: /schemas/report-document.schema.json

Subscribe to webhooks

Give us a URL and the events you care about — we'll POST a notification there the moment something is ready.

# Subscribe to events (scopes webhooks:write + webhooks:read).
# The response includes a secret shown ONCE — store it to verify signatures.
curl -X POST https://api.vibe2text.ru/api/v1/webhooks/ \
  -H 'Authorization: Bearer v2t_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://hooks.your-app.com/vibe2text",
    "events": ["transcript.completed", "smart_report.generated"]
  }'

# Lost the secret? POST /webhooks/{id}/regenerate-secret.
# Test the endpoint with POST /webhooks/{id}/retest. Inspect deliveries
# with GET /webhooks/{id}/logs. (There is no DELETE — disable via PATCH.)

Response

{
  "id": "0550f52d-...",
  "url": "https://example.com/hook",
  "events": ["transcript.completed"],
  "secret_hint": "p4-X...zkvI",
  "is_active": true,
  "last_delivery_at": null,
  "last_status_code": null,
  "created_at": "2026-06-04T00:41:21Z",
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxx"
}
# "secret" is returned ONLY on create (and regenerate-secret) — store it.
# Later reads show only "secret_hint".
Verifying the signature

Every webhook carries a signature in the X-Signature header (format sha256=<hex>). Verify it against the webhook secret over the raw body bytes (before json.loads) — that's how you confirm the message really came from us.

# Verify the signature on every incoming webhook.
# We send the signature in the "X-Signature" request header, formatted as
# "sha256=<hex>". Pass the RAW body bytes (before json.loads) to HMAC.
import hashlib
import hmac

def verify(raw_body: bytes, x_signature_header: str, secret: str) -> bool:
    if not x_signature_header or not x_signature_header.startswith("sha256="):
        return False
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=raw_body,        # raw bytes, BEFORE json.loads(...)
        digestmod=hashlib.sha256,
    ).hexdigest()
    provided = x_signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, provided)
Event list
  • transcript.created
  • transcript.completed
  • transcript.failed
  • smart_report.generated
  • export.completed
  • action_item.created
  • payment.succeeded
  • payment.failed
Rate limits & idempotency
  • File uploads. POST /files/upload-init — at most 10 requests per hour.
  • Source imports. POST /source-imports/ — at most 30 requests per hour.
  • Smart reports. At most 3 concurrent LLM jobs per user; exceeding that returns 429.
  • Idempotency-Key. Supported on upload-init and smart-reports: a key up to 128 chars, reused within 60 seconds, returns the prior result instead of starting a new one.
  • Pre-signed URLs. Valid for 3600 seconds. Re-presign an expired upload part via .../parts/:part_no/presign.
Errors

Errors are returned with an HTTP status code and a JSON body. The detail field is an array of (key, text) objects, where key is machine-readable and text is a human-readable description. Every error response carries a request_id — quote it in support tickets.

Insufficient scope (403)

The key is missing the scope the endpoint requires. Issue a key with the necessary scope.

HTTP/1.1 403 Forbidden
{
  "detail": [
    { "key": "not_enough_access_rights", "text": "API key requires scope reports:read" }
  ],
  "request_id": "<uuid>"
}
Endpoint not available via API key (403)

The method requires a signed-in web session and cannot be reached with an API key.

HTTP/1.1 403 Forbidden
{
  "detail": [
    {
      "key": "not_enough_access_rights",
      "text": "This endpoint is not available via API key — use a session login."
    }
  ],
  "request_id": "<uuid>"
}
Validation error (400 / 422)

The request body failed validation. The example below is the required scope parameter for knowledge bases.

HTTP/1.1 400 Bad Request
{
  "detail": [
    {
      "key": "invalid_input_data",
      "text": "Scope is required: pass organization_id or personal=true."
    }
  ],
  "request_id": "<uuid>"
}

request_id is present on every error response. Keep it and include it in any support request.

Support

Contact support

Send one clear note. We reply by email.

Ready to try it?

Upload a file — we'll transcribe it in a couple of minutes.