> ## 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.

# Complete Upload

> Complete an upload and wait for optional SHA-256 verification

Call this endpoint only after the S3 `PUT` succeeds. When `content_sha256` is
supplied, Bota independently reads the stored object and returns `425` while
verification is pending. Retry the identical request with a delay and a bounded
deadline. Only `200` confirms completion; retain the source if the deadline expires.

<Warning>
  Treat only `200` as permission to delete the recoverable source. `425` means
  the object is stored but server verification has not completed. A SHA mismatch
  changes the recording to `integrity_failure`; request a fresh upload URL and
  upload the complete source again.
</Warning>

## Authentication

This endpoint accepts either an API key or a device token.

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.bota.dev/v1/recordings/rec_abc123/upload-complete \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "duration_seconds": 1800,
      "file_size_bytes": 2048576,
      "content_sha256": "9b2c0f4a8e1d6c3b5a7098f6e4d2c1b0a9f8e7d6c5b4a3928172635445362718"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.bota.dev/v1/recordings/rec_abc123/upload-complete', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      duration_seconds: 1800,
      file_size_bytes: 2048576,
    }),
  });

  if (!response.ok) {
    throw new Error(`Upload completion failed: ${response.status}`);
  }

  const recording = await response.json();
  console.log(recording.status);
  ```
</RequestExample>

## Path Parameters

<ParamField path="id" type="string" required>
  The recording's unique identifier.
</ParamField>

## Request Body

<ParamField body="duration_seconds" type="integer">
  Duration of the uploaded audio in seconds.
</ParamField>

<ParamField body="file_size_bytes" type="integer">
  Uploaded file size in bytes.
</ParamField>

<ParamField body="content_sha256" type="string">
  SHA-256 of the exact bytes sent in the S3 PUT, as 64 lowercase hex characters.
  When supplied, a successful response is gated on server-side verification.
</ParamField>

<ParamField body="upload_attempt_id" type="string">
  Optional diagnostic identifier for this upload attempt (1–64 characters:
  letters, digits, period, underscore, colon or hyphen). Reuse it on completion
  retries; use a new value for a new PUT attempt. It does not change idempotency.
</ParamField>

<ParamField body="client_version" type="string">
  Optional client name/version for diagnostics (1–64 characters: letters, digits,
  period, underscore, colon, plus or hyphen). Do not include credentials or personal data.
</ParamField>

## Handling verification failures

`409 upload_integrity_failed` means the stored bytes failed verification. Keep
the source, request a fresh upload URL, PUT the complete file again, then call
completion with the SHA-256 and length of those exact bytes. Bound whole-file
retries too; repeated mismatch needs investigation. Do not replace the declared
hash with a server-reported hash merely to force acceptance.

Capture the recording ID, upload attempt ID, client version, PUT byte count and
SHA-256, completion request ID, and HTTP status in client diagnostics. If a
device also supplied a hash, compare it with the staged file before accepting
the transfer. Keep both values for diagnosis without logging audio, credentials,
or signed URLs. Bota worker logs record expected/actual hashes, byte counts and
the S3 version/ETag from the same read. Asynchronous verification errors appear
in worker logs; the earlier HTTP request can legitimately show `425` in API Logs.

## Response

Returns the updated recording object. With `content_sha256`, `200` means
`content_sha256_verified_at` has been set. Until then the endpoint returns
`425 upload_verification_pending`; clients should retry with backoff without
re-uploading. Without a supplied hash, no integrity check runs.

An exact retry after a lost or timed-out success response also returns `200`
with the already uploaded recording. The replay does not repeat storage usage,
`recording.uploaded`, auto-enhancement, auto-transcription, or integrity-queue
side effects. If supplied duration, size, or SHA-256 conflicts with a stored
non-null value, the endpoint returns `409`.

An already processed completion is not itself a conflict. Retrying the same
duration, byte count, and SHA-256 after verification succeeds must return `200`.

For a device recording whose backend policy has reached applied
`v2_required`, the first legacy completion is rejected with `409` and error code
`encrypted_upload_v2_required`, even when an older permissive credential was
issued. Any rejected unpublished plaintext final object is removed.

<ResponseExample>
  ```json 200 theme={null}
  {
    "id": "rec_abc123",
    "device_id": "dev_abc123",
    "end_user_id": "eu_xyz789",
    "name": "Product interview - Jane Doe",
    "status": "uploaded",
    "duration_seconds": 1800,
    "file_size_bytes": 2048576,
    "content_sha256": "9b2c0f4a8e1d6c3b5a7098f6e4d2c1b0a9f8e7d6c5b4a3928172635445362718",
    "content_sha256_verified_at": "2026-09-24T06:20:00Z",
    "recorded_at": "2025-01-15T09:00:00Z",
    "metadata": {
      "meeting_type": "interview"
    },
    "created_at": "2025-01-15T10:00:00Z",
    "updated_at": "2025-01-15T10:05:00Z"
  }
  ```
</ResponseExample>

<Note>
  The first completion must be in `pending` state with an assigned storage key.
  An exact replay is accepted from `uploaded` after verification. After
  `integrity_failure`, obtain a new upload URL, repeat the full PUT, then retry
  completion. Other incompatible states return `400`.
</Note>

## Ambiguous Responses

If the response is lost or times out, retry the exact request. The server
serializes the transition and returns the stored uploaded recording. A changed
request is not a replay: conflicting stored evidence returns `409`. The
`Idempotency-Key` header is not required for this recording-scoped behavior.

Already acknowledged legacy uploads can be replayed after a policy change,
without issuing a new upload credential. Published encrypted-upload-v2 rows
must use their session protocol and cannot be patched through this endpoint.
Zero duration is accepted for a recording shorter than one second. Suppressing
replayed effects does not provide a transactional, exactly-once outbox for a
server crash between the committed transition and its downstream effects.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.