Uploads

Bytes move directly to storage over presigned URLs; this API only registers what landed.

POST /v1/uploads

Open an upload and get a transfer target. No endpoint accepts media bytes: every byte moves directly to storage over a short-lived presigned URL. declared_bytes is required for upload and omitted for recording; sha256 is optional for upload and forbidden for recording. create_run defaults true and creates the linked run in status receiving in the same transaction.

Scope: uploads:write · accepts Idempotency-Key

Parameters

NameInTypeNotes
Idempotency-KeyheaderstringA value you will not reuse by accident — a UUID. A repeat within 24 hours returns the original response with its original status code (07 §3).

Request body

FieldTypeRequiredNotes
kindupload | recordingyes
filenamestringAdvisory; becomes the run title after sanitising. Never decides media type.
content_typestringAdvisory, recorded for diagnostics, never trusted.
declared_bytesintegerRequired for upload, omitted for recording. Checked against the byte ceiling before any URL is issued.
sha256stringOptional for upload; forbidden for recording. Lets the cached lane answer before a byte moves.
part_sizeintegerRequested part size in bytes; clamped to [5 MiB, 64 MiB] for multipart.
create_runbooleanDefault true.
runobject
{
  "kind": "upload",
  "filename": "board-meeting.m4a",
  "content_type": "audio/mp4",
  "declared_bytes": 148234901,
  "create_run": true
}

Responses

201The upload is open. For storage_mode single the one transfer target is inline.

FieldTypeRequiredNotes
upload_iduuidyes
kindupload | recordingyes
statusUploadStatusyes
storage_modeStorageModeyes
part_sizeinteger | null
max_partsintegeryes
expires_atdate-timeyes
run_iduuid | nullyesnull when create_run was false.
limitsobjectyes
targetTransferTarget | nullPresent for storage_mode single: the one transfer target, inline.
{
  "upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
  "kind": "upload",
  "status": "open",
  "storage_mode": "multipart",
  "part_size": 8388608,
  "max_parts": 10000,
  "expires_at": "2026-03-12T18:04:11Z",
  "run_id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
  "limits": {
    "max_run_bytes": 2147483648,
    "max_run_seconds": 28800
  },
  "target": null
}

Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), payload_too_large (413), quota_exceeded (429), engine_unavailable (503)

cURL

curl -sS -X POST https://<host>/v1/uploads \
  -H "Authorization: Bearer $SCRIPTRIP_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"kind":"upload","filename":"board-meeting.m4a","declared_bytes":148234901,"create_run":true}'

GET /v1/uploads/{id}

State of an upload. What a client asks after a crash. received_part_indexes is the complete list, so reconciliation is set arithmetic.

Scope: uploads:write

Parameters

NameInTypeNotes
idpathuuidThe upload id.

Responses

200The upload.

FieldTypeRequiredNotes
upload_iduuidyes
kindupload | recordingyes
statusUploadStatusyes
storage_modeStorageModeyes
part_countintegeryes
received_bytesintegeryes
received_part_indexesinteger[]yesThe complete list, not a range: a hole at index 12 must be expressible.
highest_contiguous_partintegeryes
has_more_partsbooleantrue only when the list was truncated at 10 000.
run_iduuid | nullyes
created_atdate-timeyes
expires_atdate-timeyes
{
  "upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
  "kind": "recording",
  "status": "open",
  "storage_mode": "parts",
  "part_count": 41,
  "received_bytes": 4975104,
  "received_part_indexes": [
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
    10,
    11,
    12,
    13,
    14,
    15,
    16,
    17,
    18,
    19,
    20,
    21,
    22,
    23,
    24,
    25,
    26,
    27,
    28,
    29,
    30,
    31,
    32,
    33,
    34,
    35,
    36,
    37,
    38,
    39,
    40,
    41
  ],
  "highest_contiguous_part": 41,
  "has_more_parts": false,
  "run_id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
  "created_at": "2026-03-11T17:12:04Z",
  "expires_at": "2026-03-12T17:12:04Z"
}

Errors: unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404)

cURL

curl -sS "https://<host>/v1/uploads/$UPLOAD_ID" -H "Authorization: Bearer $SCRIPTRIP_KEY"

POST /v1/uploads/{id}/parts

Get transfer targets, or record parts that landed. One endpoint, two operations, told apart by the presence of received. part_index is 1-based and contiguous; at most 100 targets per request; a target expires in 15 minutes. sha256 is required on every reported part; etag only in multipart mode.

Scope: uploads:write · Per account, 600 per minute — a recorder draining a backlog is a legitimate burst.

Parameters

NameInTypeNotes
idpathuuidThe upload id.

Request body

FieldTypeRequiredNotes
partsobject[]yes
{
  "parts": [
    {
      "part_index": 1,
      "bytes": 8388608,
      "sha256": "3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea",
      "etag": "\"9b2cf535f27731c974343645a3985328\"",
      "received": true
    }
  ]
}

Responses

200Targets, when the request asked for them; progress, when it recorded landed parts.

Shape 1

FieldTypeRequiredNotes
upload_iduuidyes
targetsTransferTarget[]yes

Shape 2

FieldTypeRequiredNotes
upload_iduuidyes
statusUploadStatusyes
part_countintegeryes
received_bytesintegeryes
highest_contiguous_partintegeryes
{
  "upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
  "status": "open",
  "part_count": 1,
  "received_bytes": 8388608,
  "highest_contiguous_part": 1
}

Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404), conflict (409), payload_too_large (413)

cURL

curl -sS -X POST "https://<host>/v1/uploads/$UPLOAD_ID/parts" \
  -H "Authorization: Bearer $SCRIPTRIP_KEY" -H "Content-Type: application/json" \
  -d '{"parts":[{"part_index":1},{"part_index":2}]}'

POST /v1/uploads/{id}/complete

Finish and verify. One statement with every guard in its predicate. The run becomes queued. A retried completion promotes nothing a second time and says so. 409 conflict names the actual cause: already complete, aborted, expired, a part-count mismatch, or a gap at index n.

Scope: uploads:write

Parameters

NameInTypeNotes
idpathuuidThe upload id.

Request body

FieldTypeRequiredNotes
part_countintegeryes
sha256stringOver the concatenated media in part order. Required for recording, optional for upload.
duration_secondsnumberAdvisory; lets the duration ceiling be enforced now. Overwritten by the measured value.
{
  "part_count": 18,
  "sha256": "b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7",
  "duration_seconds": 3612
}

Responses

200Verified; the run is queued.

FieldTypeRequiredNotes
upload_iduuidyes
statusUploadStatusyes
part_countintegeryes
received_bytesintegeryes
sha256string | nullyes
runRun | nullyes
messagestringyes
{
  "upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
  "status": "complete",
  "part_count": 18,
  "received_bytes": 148234901,
  "sha256": "b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7",
  "run": {
    "id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
    "source_type": "upload",
    "source_url": null,
    "source_upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
    "title": null,
    "author_name": null,
    "author_url": null,
    "description": null,
    "published_on": null,
    "duration_seconds": null,
    "thumbnail_url": null,
    "status": "queued",
    "lane": null,
    "engine": null,
    "current_step": null,
    "step_log": [],
    "language": null,
    "requested_language": null,
    "name_speakers": true,
    "segments": null,
    "segment_count": null,
    "speaker_count": null,
    "word_count": null,
    "speaker_labels": null,
    "cached_from_run_id": null,
    "media_fingerprint": "b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7",
    "error": null,
    "error_class": null,
    "retry_after": null,
    "idempotency_key": null,
    "created_at": "2026-03-11T18:04:11Z",
    "updated_at": "2026-03-11T18:04:11Z",
    "completed_at": null,
    "transcript_url": null,
    "expected_steps": null,
    "seconds_until_modal": 79,
    "live_workers": {
      "count": 2,
      "kind": "utility"
    },
    "worker_label": null,
    "machine_quiet": false
  },
  "message": "18 parts verified. The run is queued."
}

Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404), conflict (409), payload_too_large (413), unprocessable_source (422)

cURL

curl -sS -X POST "https://<host>/v1/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $SCRIPTRIP_KEY" -H "Content-Type: application/json" \
  -d '{"part_count":18}'

DELETE /v1/uploads/{id}

Abort. Aborting is never an error: 200 whether this request moved it or found it already aborted or expired. A complete upload cannot be aborted: 409 conflict. A linked run in receiving becomes canceled.

Scope: uploads:write

Parameters

NameInTypeNotes
idpathuuidThe upload id.

Responses

200The state, and whether this request changed it.

FieldTypeRequiredNotes
upload_iduuidyes
statusUploadStatusyes
runobject | nullyes
messagestringyes
{
  "upload_id": "0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab",
  "status": "aborted",
  "run": {
    "id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
    "status": "canceled"
  },
  "message": "The upload was aborted and its run was canceled."
}

Errors: unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404), conflict (409)

cURL

curl -sS -X DELETE "https://<host>/v1/uploads/$UPLOAD_ID" -H "Authorization: Bearer $SCRIPTRIP_KEY"