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"
}'
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);
{
"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"
}
Uploads
Complete Upload
Complete an upload and wait for optional SHA-256 verification
POST
/
recordings
/
{id}
/
upload-complete
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"
}'
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);
{
"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"
}
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.
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.Authentication
This endpoint accepts either an API key or a device token.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"
}'
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);
Path Parameters
string
required
The recording’s unique identifier.
Request Body
integer
Duration of the uploaded audio in seconds.
integer
Uploaded file size in bytes.
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.
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.
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.
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. Withcontent_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.
{
"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"
}
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.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 returns409. 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.Was this page helpful?

