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

# Finalize Streaming Recording

> Complete a streaming recording, stitch audio chunks, and trigger transcription.

Finalizes a streaming recording after all chunks have been uploaded. This endpoint marks the recording as complete, queues a background job to stitch audio chunks into a single file, and triggers transcription once stitching is complete.

## Authentication

Requires a valid [device token](/authentication) or [API key](/authentication) with `recordings:write` scope.

<RequestExample>
  ```bash cURL (Device Token) theme={null}
  curl -X POST https://api.bota.dev/v1/recordings/rec_abc123xyz/finalize \
    -H "Authorization: Bearer dtok_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "total_chunks": 12,
      "final_duration_ms": 3595420,
      "checksum_md5": "5d41402abc4b2a76b9719d911017c592"
    }'
  ```

  ```bash cURL (API Key) theme={null}
  curl -X POST https://api.bota.dev/v1/recordings/rec_abc123xyz/finalize \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "total_chunks": 12,
      "final_duration_ms": 3595420
    }'
  ```

  ```javascript Node.js theme={null}
  const result = await bota.recordings.finalizeStreaming('rec_abc123xyz', {
    total_chunks: 12,
    final_duration_ms: 3595420,
    checksum_md5: '5d41402abc4b2a76b9719d911017c592'
  });

  console.log(result.status); // "uploaded"
  console.log(result.transcription_status); // "queued"
  ```

  ```python Python theme={null}
  result = bota.recordings.finalize_streaming('rec_abc123xyz', {
    'total_chunks': 12,
    'final_duration_ms': 3595420,
    'checksum_md5': '5d41402abc4b2a76b9719d911017c592'
  })

  print(result['status'])  # "uploaded"
  print(result['transcription_status'])  # "queued"
  ```
</RequestExample>

## Path Parameters

<ParamField path="id" type="string" required>
  Recording ID from the initial streaming recording creation (e.g., `rec_abc123xyz`)
</ParamField>

## Request Body

<ParamField body="total_chunks" type="integer" required>
  Total number of chunks uploaded (must match actual chunk count)
</ParamField>

<ParamField body="final_duration_ms" type="integer" required>
  Actual recording duration in milliseconds
</ParamField>

<ParamField body="checksum_md5" type="string">
  MD5 checksum of the complete audio (optional, for integrity verification)
</ParamField>

<ParamField body="content_sha256" type="string">
  SHA-256 of the complete audio bytes as a 64-character lowercase hex string (optional). When supplied, the server recomputes the SHA-256 of the assembled object after chunk stitching and marks the recording `status=integrity_failure` on mismatch.
</ParamField>

<ParamField body="trailer_base64" type="string">
  Optional base64-encoded bytes to append to the assembled audio after all chunks. Use this if your encoder writes bytes at close that were not captured in the chunk stream — for example, the final-page flush and EOS page that OGG/Opus encoders emit during `fclose()`. Without this, those bytes are absent from the assembled S3 object and `content_sha256` verification will fail even though the audio was transmitted correctly. Max 16384 base64 characters (≈12KB raw).
</ParamField>

## Response

Returns the finalized recording status.

<ResponseExample>
  ```json Success Response theme={null}
  {
    "id": "rec_abc123xyz",
    "status": "uploaded",
    "transcription_status": "queued",
    "chunks_received": 12,
    "stitching_job_id": "job_stitcher_abc123"
  }
  ```

  ```json Recording Not Found theme={null}
  {
    "error": {
      "type": "recording_not_found",
      "message": "Streaming recording not found"
    }
  }
  ```

  ```json Not a Streaming Recording theme={null}
  {
    "error": {
      "type": "invalid_operation",
      "message": "Recording was not created with streaming enabled"
    }
  }
  ```

  ```json Chunk Count Mismatch theme={null}
  {
    "error": {
      "type": "chunk_mismatch",
      "message": "Expected 12 chunks, but only 10 were uploaded",
      "details": {
        "expected_chunks": 12,
        "received_chunks": 10,
        "missing_chunks": [7, 11]
      }
    }
  }
  ```

  ```json Already Finalized theme={null}
  {
    "error": {
      "type": "already_finalized",
      "message": "Recording has already been finalized"
    }
  }
  ```
</ResponseExample>

## Response Fields

| Field                  | Type    | Description                                                             |
| ---------------------- | ------- | ----------------------------------------------------------------------- |
| `id`                   | string  | Recording ID.                                                           |
| `status`               | string  | Recording status: `uploaded`, `stitching`, or `failed`.                 |
| `transcription_status` | string  | Transcription status: `queued`, `processing`, `completed`, or `failed`. |
| `chunks_received`      | integer | Number of chunks received by the backend.                               |
| `stitching_job_id`     | string  | Background job ID for chunk stitching (for debugging).                  |

## Background Processing

After calling `/finalize`, the following happens asynchronously:

```mermaid theme={null}
sequenceDiagram
    Device->>API: POST /finalize
    API->>Queue: Queue chunk stitching job
    API-->>Device: { status: "uploaded" }
    Queue->>Worker: Process stitching
    Worker->>S3: Download chunks
    Worker->>Worker: Concatenate audio
    Worker->>S3: Upload final file
    Worker->>Queue: Queue transcription
    Queue->>Worker: Process transcription
    Worker->>Whisper: Transcribe audio
    Worker->>Database: Save transcription
    Worker->>Webhook: Dispatch transcription.completed
```

**Typical Processing Times:**

* Chunk stitching: 5-30 seconds (depending on chunk count)
* Transcription: 1-5 minutes (depending on duration)

## Webhooks

You'll receive webhooks for:

1. **`recording.uploaded`** - Fired immediately after finalization
2. **`transcription.queued`** - Transcription job queued
3. **`transcription.completed`** - Transcription finished successfully
4. **`transcription.failed`** - Transcription failed

## Streaming Upload Example (Full Flow)

### Step 1: Create Streaming Recording

```bash theme={null}
POST /v1/recordings
Authorization: Bearer dtok_live_...

{
  "started_at": "2025-01-13T10:00:00Z",
  "estimated_duration_ms": 3600000,
  "codec": "opus",
  "streaming": true,
  "chunk_duration_ms": 300000
}
```

> **Note:** When using device tokens (`dtok_*`), `device_id` and `end_user_id` are automatically extracted from the token. When using API keys (`sk_*`), include `device_id` and `end_user_id` in the request body.

**Response:**

```json theme={null}
{
  "id": "rec_abc123",
  "status": "streaming",
  "chunk_upload_url_template": "https://s3.../rec_abc123/chunk_{part}.opus?...",
  "max_chunks": 100,
  "expires_at": "2025-01-13T14:00:00Z"
}
```

### Step 2: Upload Chunks (Device)

```bash theme={null}
# Upload chunk 1 (0-5 minutes)
PUT https://s3.../rec_abc123/chunk_1.opus?...
[binary audio data]

# Upload chunk 2 (5-10 minutes)
PUT https://s3.../rec_abc123/chunk_2.opus?...
[binary audio data]

# ... continue uploading chunks ...

# Upload chunk 12 (55-60 minutes)
PUT https://s3.../rec_abc123/chunk_12.opus?...
[binary audio data]
```

### Step 3: Finalize Recording

```bash theme={null}
POST /v1/recordings/rec_abc123/finalize
{
  "total_chunks": 12,
  "final_duration_ms": 3595420,
  "checksum_md5": "5d41402abc4b2a76b9719d911017c592"
}
```

**Response:**

```json theme={null}
{
  "id": "rec_abc123",
  "status": "uploaded",
  "transcription_status": "queued",
  "chunks_received": 12
}
```

### Step 4: Wait for Webhooks

```json theme={null}
// Webhook 1: recording.uploaded
{
  "type": "recording.uploaded",
  "data": {
    "id": "rec_abc123",
    "uploaded_at": "2025-01-13T11:00:15Z"
  }
}

// Webhook 2: transcription.completed
{
  "type": "transcription.completed",
  "data": {
    "id": "txn_def456",
    "recording_id": "rec_abc123",
    "status": "completed"
  }
}
```

## Best Practices

<Check>
  **Verify Chunk Count:** Ensure `total_chunks` matches the number of chunks you actually uploaded. The backend will verify chunk existence before stitching.
</Check>

<Check>
  **Include Checksum:** Provide an MD5 checksum for integrity verification, especially for critical recordings (e.g., medical, legal).
</Check>

<Warning>
  **Don't Finalize Early:** Only call `/finalize` after all chunks are uploaded. Finalizing with missing chunks will result in an error.
</Warning>

<Info>
  **Idempotency:** Calling `/finalize` multiple times with the same parameters is safe (idempotent). Duplicate requests will return the same response without re-processing.
</Info>

## Troubleshooting

**Problem:** "Chunk count mismatch" error

**Solution:** Check that all chunks were uploaded successfully. Use the error response to identify missing chunks.

```json theme={null}
{
  "error": {
    "type": "chunk_mismatch",
    "missing_chunks": [7, 11]  // Re-upload these chunks
  }
}
```

**Problem:** Stitching takes too long

**Solution:** This is normal for recordings with many chunks (>50). Check stitching job status via webhooks or polling the recording endpoint.

## Related Endpoints

* [Create Recording](/api-reference/recordings/create) - Create streaming recording
* [Get Recording](/api-reference/recordings/get) - Check stitching status
* [Create Transcription](/api-reference/transcriptions/create) - Manual transcription trigger
