> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gnani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Reference batchSTT restructured

# Speech-to-Text (Batch)

Use **STT Batch** to transcribe long recordings or many files asynchronously.

**Flow:** upload audio → start a job → poll until it finishes → download transcripts\
(or receive them on a webhook).

| If you need…                                       | Use                                        |
| -------------------------------------------------- | ------------------------------------------ |
| Long files, many files, or offline pipelines       | **STT Batch** (this page)                  |
| Short clips (under \~60 seconds), immediate answer | [STT REST](/vachana/STT/speech-to-text)    |
| Live microphone / streaming audio                  | [STT Realtime](/vachana/STT/stt-websocket) |

***

## Table of contents

1. [How it works](#how-it-works)
2. [Before you begin](#before-you-begin)
3. [Quickstart](#quickstart-copy-paste)
4. [How do I get the transcript text?](#how-do-i-get-the-transcript-text)
5. [Choose how to send audio](#choose-how-to-send-audio)
6. [Recipes](#recipes)
7. [Configuration](#configuration)
8. [Limits](#limits)
9. [Supported languages](#supported-languages)
10. [Supported audio formats](#supported-audio-formats)
11. [API reference](#api-reference)
12. [Job statuses](#job-statuses)
13. [File statuses](#file-statuses)
14. [Webhooks](#webhooks)
15. [Errors](#errors)
16. [Common mistakes](#common-mistakes)
17. [Python example](#python-example)
18. [Support](#support)

***

## How it works

```text theme={null}
1. CREATE    POST /stt/v3/batch/jobs
             → 201  job_id   status: CREATED

2. START     POST /stt/v3/batch/jobs/{job_id}/start
             → 202             status: STARTING

3. WAIT      GET  /stt/v3/batch/jobs/{job_id}
             poll every 10s until a terminal status

4. FILES     GET  /stt/v3/batch/jobs/{job_id}/files
             → transcript_url for each completed file
             (no transcript text in this response)

5. DOWNLOAD  GET  <transcript_url>
             → JSON with full_transcript + segments
             (no API key needed)
```

**Important:** creating a job does **not** start transcription. You must call `/start`.

***

## Before you begin

1. An API key from the Gnani APIs dashboard
2. Audio in a [supported format](#supported-audio-formats)
3. A [supported language code](#supported-languages)

**Base URL**

```text theme={null}
https://api.vachana.ai
```

**Auth** — send on every Batch API request. This is the only required header:

```http theme={null}
X-API-Key-ID: <YOUR_API_KEY>
```

***

## Quickstart (copy-paste)

### 1. Create the job

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":false,"is_multi_channel":false};type=application/json' \
  -F "files=@./customer_call.wav"
```

**Response — `201 Created`**

```json theme={null}
{
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "status": "CREATED",
  "total_files_accepted": 1,
  "created_at": "2026-07-27T06:53:34.750193Z",
  "message": "1 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing."
}
```

Save `job_id`. Always start with this path (even if the message mentions `/v1/jobs/...`):

```text theme={null}
POST /stt/v3/batch/jobs/{job_id}/start
```

### 2. Start processing

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6/start" \
  -H "X-API-Key-ID: <YOUR_API_KEY>"
```

**Response — `202 Accepted`**

```json theme={null}
{
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "status": "STARTING",
  "message": "Job start accepted. File validation and queueing are running in the background. Poll GET /stt/v3/batch/jobs/{job_id} to track progress."
}
```

### 3. Poll status

* Poll no faster than every **10 seconds**
* For large jobs (100+ files), prefer every **30 seconds**
* Stop when status is `COMPLETED`, `PARTIAL_FAILURE`, `FAILED`, `START_FAILED`, or `CANCELLED`

```bash theme={null}
curl "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6" \
  -H "X-API-Key-ID: <YOUR_API_KEY>"
```

**Response — `200 OK` (in progress)**

```json theme={null}
{
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "status": "IN_PROGRESS",
  "config": {
    "model": "gnani-prisma-v2.5",
    "mode": "transcribe",
    "language_code": "en-IN",
    "with_diarization": false,
    "num_speakers": null,
    "is_multi_channel": false
  },
  "progress": {
    "total_files": 1,
    "completed_files": 0,
    "failed_files": 0,
    "in_progress_files": 0,
    "queued_files": 1,
    "cancelled_files": 0
  },
  "cancel_reason": null,
  "created_at": "2026-07-27T06:53:34.750193Z",
  "started_at": "2026-07-27T06:53:37.276061Z",
  "completed_at": null,
  "cancelled_at": null,
  "updated_at": "2026-07-27T06:53:45.000000Z"
}
```

**Response — `200 OK` (done)**

```json theme={null}
{
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "status": "COMPLETED",
  "config": {
    "model": "gnani-prisma-v2.5",
    "mode": "transcribe",
    "language_code": "en-IN",
    "with_diarization": false,
    "num_speakers": null,
    "is_multi_channel": false
  },
  "progress": {
    "total_files": 1,
    "completed_files": 1,
    "failed_files": 0,
    "in_progress_files": 0,
    "queued_files": 0,
    "cancelled_files": 0
  },
  "cancel_reason": null,
  "created_at": "2026-07-27T06:53:34.750193Z",
  "started_at": "2026-07-27T06:53:37.276061Z",
  "completed_at": "2026-07-27T06:53:52.179794Z",
  "cancelled_at": null,
  "updated_at": "2026-07-27T06:53:52.179928Z"
}
```

Job status does **not** include transcript text.

| Status            | Meaning                 | Next step                                     |
| ----------------- | ----------------------- | --------------------------------------------- |
| `COMPLETED`       | All files succeeded     | Call `/files`, then GET each `transcript_url` |
| `PARTIAL_FAILURE` | Mixed success / failure | Same — fetch successful files via `/files`    |
| `FAILED`          | Job failed              | Inspect `/files` + `error_message`            |
| `START_FAILED`    | Could not start         | Read `cancel_reason`                          |
| `CANCELLED`       | Cancelled               | Done                                          |

### 4. Get completed files

This response gives you `transcript_url` — **not** the transcript text yet.

```bash theme={null}
curl "https://api.vachana.ai/stt/v3/batch/jobs/019fa259-b084-71eb-94d7-ba4cdf2e64c6/files?status=COMPLETED" \
  -H "X-API-Key-ID: <YOUR_API_KEY>"
```

**Response — `200 OK`**

```json theme={null}
{
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "data": [
    {
      "file_id": "28255118-03ca-48c5-807e-8d07a6f54d54",
      "original_path": "customer_call.wav",
      "status": "COMPLETED",
      "duration_seconds": "6.01",
      "transcript_url": "https://gnaniasraudios.s3.amazonaws.com/results/.../28255118-03ca-48c5-807e-8d07a6f54d54.json?X-Amz-Expires=3600&...",
      "error_message": null,
      "created_at": "2026-07-27T06:53:34.750193Z",
      "completed_at": "2026-07-27T06:53:52.128197Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null,
    "total_count": 1
  }
}
```

### 5. Download the transcript (`full_transcript`)

If you are **not** using webhooks, this is the step that returns the text.

Call `transcript_url` with a normal HTTP GET. Do **not** send `X-API-Key-ID` — the URL is pre-signed.

```bash theme={null}
curl -L "<transcript_url_from_step_4>"
```

**Response — transcript JSON**

```json theme={null}
{
  "file_id": "28255118-03ca-48c5-807e-8d07a6f54d54",
  "job_id": "019fa259-b084-71eb-94d7-ba4cdf2e64c6",
  "original_path": "customer_call.wav",
  "language_code": "en-IN",
  "duration_seconds": 6.01,
  "model": "gnani-prisma-v2.5",
  "mode": "transcribe",
  "segments": [
    {
      "segment_id": 0,
      "start_time": 0.0,
      "end_time": 0.71,
      "text": "hello",
      "speaker_id": 1,
      "confidence": null,
      "language_detected": null,
      "sentiment": null,
      "emotion": null
    }
  ],
  "full_transcript": "hello how can i help you today i am calling about my recent order number 12345",
  "created_at": "2026-07-27T06:53:52Z"
}
```

* Use `full_transcript` for the complete text
* Use `segments` for timestamps / speaker labels
* `transcript_url` expires in **1 hour** — re-call `/files` for a fresh URL

***

## How do I get the transcript text?

| Option                   | When to use               | What you do                                                                                                                 |
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **A. Polling (default)** | No webhook                | 1) Poll job status until terminal → 2) `GET /jobs/{job_id}/files` → 3) `GET` each `transcript_url` → read `full_transcript` |
| **B. Webhook**           | You passed `callback_url` | Receive a POST with `transcripts[]` already including `full_transcript` (keep polling as backup)                            |

`GET /jobs/{job_id}` and `GET /jobs/{job_id}/files` do **not** return `full_transcript`.\
`/files` only returns `transcript_url`. You must GET that URL to get the text.

***

## Choose how to send audio

| Method            | Best for                                 | Content-Type          |
| ----------------- | ---------------------------------------- | --------------------- |
| **Direct upload** | Files on your machine / CI               | `multipart/form-data` |
| **ZIP upload**    | Many files in one archive                | `multipart/form-data` |
| **Public URL**    | HTTPS links, including S3 presigned URLs | `application/json`    |

Only these three methods are supported. Authenticated private bucket access (IAM AssumeRole, GCS keys, Azure keys) is **not** available.

***

## Recipes

### Multiple files

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe"};type=application/json' \
  -F "files=@./call_001.wav" \
  -F "files=@./call_002.wav"
```

**Response — `201 Created`**

```json theme={null}
{
  "job_id": "019fa263-99d2-7704-a7f8-3d17acc6d011",
  "status": "CREATED",
  "total_files_accepted": 2,
  "created_at": "2026-07-27T07:04:24.323433Z",
  "message": "2 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing."
}
```

### ZIP upload

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe"};type=application/json' \
  -F "files=@./calls.zip"
```

**Response — `201 Created`**

```json theme={null}
{
  "job_id": "019fa263-9c99-7875-8699-175d289cd15a",
  "status": "CREATED",
  "total_files_accepted": 2,
  "created_at": "2026-07-27T07:04:25.046413Z",
  "message": "2 file(s) uploaded. Call POST /v1/jobs/{job_id}/start to begin processing."
}
```

### Public URL

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \
  -H "Content-Type: application/json" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -d '{
    "config": {
      "model": "gnani-prisma-v2.5",
      "language_code": "en-IN",
      "mode": "transcribe"
    },
    "source": {
      "type": "cloud_storage",
      "auth": { "mode": "public" },
      "paths": ["https://cdn.example.com/audio/call-001.mp3"]
    }
  }'
```

URLs are validated at `/start`, not at create. If every path fails:

```json theme={null}
{
  "status": "START_FAILED",
  "cancel_reason": "All provided paths were invalid — nothing to process."
}
```

### Two-speaker diarization

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -F 'config={"model":"gnani-prisma-v2.5","language_code":"en-IN","mode":"transcribe","with_diarization":true,"num_speakers":2};type=application/json' \
  -F "files=@./stereo_call.wav"
```

* Send `num_speakers` when `with_diarization` is `true`
* Max speakers: **2**
* Set `is_multi_channel: true` only for true multi-channel audio (for example stereo A/B); leave `false` for mono

### Cancel before start

```bash theme={null}
curl -X POST "https://api.vachana.ai/stt/v3/batch/jobs/{job_id}/cancel" \
  -H "X-API-Key-ID: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  --data-raw '{"reason":"No longer needed"}'
```

**Response — `200 OK`**

```json theme={null}
{
  "job_id": "019fa262-971e-766b-95f0-ea57d8e0626d",
  "status": "CANCELLED",
  "message": "Job cancelled successfully.",
  "cancelled_at": "2026-07-27T07:03:18.827605Z"
}
```

***

## Configuration

| Field              | Type    | Required                  | Default        | Description                                                      |
| ------------------ | ------- | ------------------------- | -------------- | ---------------------------------------------------------------- |
| `model`            | string  | Yes                       | —              | `"gnani-prisma-v2.5"`                                            |
| `language_code`    | string  | Yes                       | —              | See [Supported languages](#supported-languages)                  |
| `mode`             | string  | No                        | `"transcribe"` | Use `"transcribe"` for now                                       |
| `with_diarization` | boolean | No                        | `false`        | Enable speaker diarization                                       |
| `num_speakers`     | integer | Yes, if diarization is on | —              | Required when `with_diarization` is `true`. Max **2**            |
| `is_multi_channel` | boolean | No                        | `false`        | `true` only if channels are separate speakers; otherwise `false` |

Inverse Text Normalization (ITN) is **not** supported for STT Batch.

***

## Limits

### Direct upload

| Limit                   | Value              | Notes                                    |
| ----------------------- | ------------------ | ---------------------------------------- |
| Max audio files per job | **100**            | Individual files + ZIP extracts combined |
| Max file size           | **10 MB** per file | Also applies to each file inside a ZIP   |

### ZIP upload

| Limit                       | Value      | Notes                               |
| --------------------------- | ---------- | ----------------------------------- |
| Max compressed ZIP size     | **50 MB**  | Uploaded `.zip` size                |
| Max total decompressed size | **200 MB** | Across all extracted entries        |
| Max files extracted per ZIP | **100**    | Shared with the 100-file job cap    |
| Max compression ratio       | **50:1**   | Higher ratios rejected as zip bombs |

### Transcript URL

| Parameter               | Value                      |
| ----------------------- | -------------------------- |
| `transcript_url` expiry | **3,600 seconds (1 hour)** |

`transcript_url` is created when you call `GET /jobs/{job_id}/files`. Re-call `/files` for a fresh URL. No API key is needed to download from the URL.

### Polling

* Poll `GET /jobs/{job_id}` no faster than every **10 seconds**
* For 100+ file jobs, prefer every **30 seconds**
* Stop on a terminal status

***

## Supported languages

| Code    | Language             |
| ------- | -------------------- |
| `bn-BD` | Bengali (Bangladesh) |
| `bn-IN` | Bengali (India)      |
| `en-IN` | English (India)      |
| `hi-IN` | Hindi                |
| `kn-IN` | Kannada              |
| `ml-IN` | Malayalam            |
| `mr-IN` | Marathi              |
| `ta-IN` | Tamil                |
| `te-IN` | Telugu               |

**Example error — `400 UNSUPPORTED_LANGUAGE`**

```json theme={null}
{
  "error": "UNSUPPORTED_LANGUAGE",
  "message": "Value error, This service does not support language_code 'xx-XX'. Supported languages: bn-BD, bn-IN, en-IN, hi-IN, kn-IN, ml-IN, mr-IN, ta-IN, te-IN"
}
```

***

## Supported audio formats

`.wav` · `.mp3` · `.mp4` · `.flac` · `.ogg` · `.opus` · `.m4a` · `.aac` · `.webm` · `.amr`

Tone-only or silent audio may be accepted into a job but later fail with `Empty transcript after 3 retries`. Use real speech.

***

## API reference

### Create job — upload / ZIP

```http theme={null}
POST /stt/v3/batch/jobs
Content-Type: multipart/form-data
```

| Field          | Required | Description                                       |
| -------------- | -------- | ------------------------------------------------- |
| `config`       | Yes      | JSON string — see [Configuration](#configuration) |
| `files`        | Yes      | Audio or ZIP. Repeat the field for multiple files |
| `callback_url` | No       | HTTPS webhook URL                                 |

### Create job — public URL

```http theme={null}
POST /stt/v3/batch/jobs
Content-Type: application/json
```

| Field              | Required | Description       |
| ------------------ | -------- | ----------------- |
| `config`           | Yes      | STT config object |
| `source.type`      | Yes      | `"cloud_storage"` |
| `source.auth.mode` | Yes      | `"public"`        |
| `source.paths`     | Yes      | HTTPS URLs        |
| `callback_url`     | No       | Webhook URL       |

### Start job

```http theme={null}
POST /stt/v3/batch/jobs/{job_id}/start
```

**Example conflict — `409`**

```json theme={null}
{
  "error": "JOB_ALREADY_STARTED",
  "message": "Job cannot be restarted from state 'COMPLETED'"
}
```

### Get job status

```http theme={null}
GET /stt/v3/batch/jobs/{job_id}
```

See [Quickstart step 3](#3-poll-status) for response shapes.

### List jobs

```http theme={null}
GET /stt/v3/batch/jobs?limit=5
```

| Query    | Default | Description                   |
| -------- | ------- | ----------------------------- |
| `status` | —       | Filter by status              |
| `limit`  | `20`    | 1–100                         |
| `cursor` | —       | From `pagination.next_cursor` |

### Get file results

```http theme={null}
GET /stt/v3/batch/jobs/{job_id}/files
```

| Query    | Default | Description       |
| -------- | ------- | ----------------- |
| `status` | —       | e.g. `COMPLETED`  |
| `limit`  | `50`    | 1–100             |
| `cursor` | —       | Pagination cursor |

Without webhooks:

1. Call this after the job is terminal
2. Take `transcript_url` for each `COMPLETED` file
3. `GET` that URL → read `full_transcript`

This response never contains `full_transcript` itself.

### Cancel job

```http theme={null}
POST /stt/v3/batch/jobs/{job_id}/cancel
```

Optional JSON body: `{ "reason": "..." }` with `Content-Type: application/json`.

| Current status                        | Result                                        |
| ------------------------------------- | --------------------------------------------- |
| `CREATED`                             | Immediately `CANCELLED` (`200`)               |
| `STARTING` / `QUEUED` / `IN_PROGRESS` | `CANCELLING` → `CANCELLED`                    |
| Already `CANCELLED`                   | `409 JOB_CANCELLED`                           |
| Other terminal states                 | Conflict error (e.g. `JOB_ALREADY_COMPLETED`) |

***

## Job statuses

| Status            | Terminal | Meaning                            |
| ----------------- | -------- | ---------------------------------- |
| `CREATED`         | No       | Waiting for `/start`               |
| `STARTING`        | No       | Setup running                      |
| `QUEUED`          | No       | Files queued                       |
| `IN_PROGRESS`     | No       | Transcription running              |
| `COMPLETED`       | Yes      | All succeeded                      |
| `PARTIAL_FAILURE` | Yes      | Mixed results                      |
| `FAILED`          | Yes      | Failed                             |
| `START_FAILED`    | Yes      | Setup failed — see `cancel_reason` |
| `CANCELLING`      | No       | Cancel in progress                 |
| `CANCELLED`       | Yes      | Cancelled                          |

***

## File statuses

| Status        | Meaning                                   |
| ------------- | ----------------------------------------- |
| `QUEUED`      | Waiting                                   |
| `IN_PROGRESS` | Transcribing                              |
| `COMPLETED`   | Done — `transcript_url` available         |
| `FAILED`      | Failed — see `error_message`              |
| `SKIPPED`     | Not processed (bad URL / format / access) |
| `CANCELLED`   | Skipped due to job cancel                 |

***

## Webhooks

Pass `callback_url` at create time to receive a `POST` when the job reaches a terminal state.

| `event`               | Triggered when                                 |
| --------------------- | ---------------------------------------------- |
| `job.completed`       | All files succeeded                            |
| `job.partial_failure` | At least one failed and at least one succeeded |
| `job.failed`          | All files failed                               |
| `job.cancelled`       | Job cancelled                                  |

The `transcripts` array includes full per-file transcript JSON (with `full_transcript`), so webhook users usually do not need `/files` + `transcript_url`.

If you are **not** using webhooks, see [How do I get the transcript text?](#how-do-i-get-the-transcript-text).

Treat webhooks as best-effort. Polling remains the source of truth.

### Sample webhook payload

```json theme={null}
{
  "event": "job.completed",
  "job_id": "019f5aab-3ef5-799e-a289-b72201d8701b",
  "org_id": "org_acme123",
  "user_id": "user_abc456",
  "status": "COMPLETED",
  "total_files": 2,
  "completed_files": 2,
  "failed_files": 0,
  "cancelled_files": 0,
  "completed_at": "2026-07-27T05:30:00Z",
  "callback_url": "https://yourapp.com/webhooks/stt",
  "transcripts": [
    {
      "file_id": "b2c3d4e5-f6a7-4901-b234-56c7d8e9f0a1",
      "job_id": "019f5aab-3ef5-799e-a289-b72201d8701b",
      "original_path": "call_001.wav",
      "language_code": "en-IN",
      "duration_seconds": 48.6,
      "model": "gnani-prisma-v2.5",
      "mode": "transcribe",
      "segments": [
        {
          "segment_id": 1,
          "start_time": 0.0,
          "end_time": 4.8,
          "text": "Hello how can I help you today",
          "speaker_id": 1,
          "confidence": 0.92,
          "language_detected": "en-IN",
          "sentiment": "neutral",
          "emotion": "calm"
        },
        {
          "segment_id": 2,
          "start_time": 5.1,
          "end_time": 9.3,
          "text": "I am calling about my recent order",
          "speaker_id": 2,
          "confidence": 0.89,
          "language_detected": "en-IN",
          "sentiment": "neutral",
          "emotion": "neutral"
        }
      ],
      "full_transcript": "Hello how can I help you today I am calling about my recent order",
      "created_at": "2026-07-27T05:30:00Z"
    }
  ]
}
```

***

## Errors

**Missing API key — `401`**

```json theme={null}
{
  "detail": {
    "error_code": "MISSING_API_KEY",
    "message": "Missing API key",
    "status_code": 401
  }
}
```

**Unsupported language — `400`**

```json theme={null}
{
  "error": "UNSUPPORTED_LANGUAGE",
  "message": "Value error, This service does not support language_code 'xx-XX'. Supported languages: bn-BD, bn-IN, en-IN, hi-IN, kn-IN, ml-IN, mr-IN, ta-IN, te-IN"
}
```

**Restart finished job — `409`**

```json theme={null}
{
  "error": "JOB_ALREADY_STARTED",
  "message": "Job cannot be restarted from state 'COMPLETED'"
}
```

**Job not found — `404`**

```json theme={null}
{
  "error": "JOB_NOT_FOUND",
  "message": "Job 01900000-0000-7000-8000-000000000000 not found"
}
```

**Already cancelled — `409`**

```json theme={null}
{
  "error": "JOB_CANCELLED",
  "message": "Job is already cancelled"
}
```

| HTTP  | Code                                  | What to do                                                          |
| ----- | ------------------------------------- | ------------------------------------------------------------------- |
| `400` | `INVALID_CONFIG`                      | Fix `config` JSON / required fields                                 |
| `400` | `INVALID_SOURCE`                      | For JSON create, include a valid `source` (or use multipart upload) |
| `400` | `NO_FILES_PROVIDED`                   | Attach at least one file                                            |
| `400` | `UNSUPPORTED_LANGUAGE`                | Use a supported language code                                       |
| `400` | `FILE_TOO_LARGE` / `ZIP_*`            | Respect upload limits                                               |
| `401` | `MISSING_API_KEY` / `INVALID_API_KEY` | Check `X-API-Key-ID`                                                |
| `404` | `JOB_NOT_FOUND`                       | Wrong `job_id`                                                      |
| `409` | `JOB_ALREADY_STARTED`                 | Do not call `/start` twice                                          |
| `409` | `JOB_CANCELLED`                       | Job already cancelled                                               |
| `422` | `NO_VALID_FILES`                      | Every path was skipped                                              |
| `429` | `RATE_LIMITED`                        | Back off and retry                                                  |
| `502` | `SERVICE_UNAVAILABLE`                 | Retry `/start` shortly                                              |

***

## Common mistakes

| Mistake                                           | What happens                | Fix                                      |
| ------------------------------------------------- | --------------------------- | ---------------------------------------- |
| Forgetting `/start`                               | Job stays `CREATED`         | Call start after create                  |
| Expecting `full_transcript` on status or `/files` | Field is missing            | `GET /files` → then `GET transcript_url` |
| Sending `X-API-Key-ID` on `transcript_url`        | Unnecessary                 | Pre-signed URL needs no API headers      |
| Using `/v1/jobs/...` from the create message      | Wrong path                  | Use `/stt/v3/batch/jobs/{job_id}/start`  |
| Polling every 1–2 seconds                         | Wasted calls                | Poll every 10s (30s for large jobs)      |
| Using an expired `transcript_url`                 | Download fails              | Re-call `/files`                         |
| Stopping at `COMPLETED` without `/files`          | No transcript retrieved     | Fetch files, then each URL               |
| Diarization without `num_speakers`                | Incomplete config           | Always send `num_speakers` (max 2)       |
| Private cloud credentials in the request          | Not supported               | Use direct upload, ZIP, or public URL    |
| Uploading silence / tones                         | `FAILED` — empty transcript | Use real speech                          |
| Checking only job status                          | Miss skipped/failed files   | Always inspect `/files`                  |

***

## Python example

```python theme={null}
import time
import httpx

BASE_URL = "https://api.vachana.ai/stt/v3/batch/jobs"
HEADERS = {"X-API-Key-ID": "<YOUR_API_KEY>"}
TERMINAL = {"COMPLETED", "PARTIAL_FAILURE", "FAILED", "START_FAILED", "CANCELLED"}

# 1) Create
with open("customer_call.wav", "rb") as f:
    resp = httpx.post(
        BASE_URL,
        headers=HEADERS,
        data={
            "config": (
                '{"model":"gnani-prisma-v2.5",'
                '"language_code":"en-IN",'
                '"mode":"transcribe",'
                '"with_diarization":false,'
                '"is_multi_channel":false}'
            )
        },
        files={"files": ("customer_call.wav", f, "audio/wav")},
    )
    resp.raise_for_status()
    job_id = resp.json()["job_id"]

# 2) Start
resp = httpx.post(f"{BASE_URL}/{job_id}/start", headers=HEADERS)
resp.raise_for_status()

# 3) Poll
while True:
    resp = httpx.get(f"{BASE_URL}/{job_id}", headers=HEADERS)
    resp.raise_for_status()
    body = resp.json()
    if body["status"] in TERMINAL:
        break
    time.sleep(10)

# 4) Files → transcript_url → full_transcript
resp = httpx.get(
    f"{BASE_URL}/{job_id}/files",
    params={"status": "COMPLETED"},
    headers=HEADERS,
)
resp.raise_for_status()
for file in resp.json()["data"]:
    if file.get("transcript_url"):
        transcript = httpx.get(file["transcript_url"]).json()
        print(transcript["full_transcript"])
```

***

## Support

Docs feedback: [Batch STT docs feedback](https://discord.com/channels/1511338519899930634/1515991436229611571)

**Last verified:** 28 Jul 2026 against `https://api.vachana.ai`
