Skip to content

Ask AI

Ask a question about Scribewren, such as uploading a file, editing a transcript, plans or the API.

Ctrl or Cmd+Enter to send

Answers are written by AI and can be wrong. For account or billing help, use the chat or contact us. Contact support (opens in a new tab)

Uploads

Media bytes never pass through the API. You upload them in parts straight to storage, using presigned URLs that the API hands out. The flow has four calls:

  1. POST /media registers the file and returns how to split it.
  2. POST /media/{id}/parts returns a presigned URL for each part you ask for. You PUT the bytes to that URL.
  3. POST /media/{id}/complete assembles the parts and queues the transcription.
  4. Optionally, DELETE /media/{id} aborts the upload.

Quickstart runs these calls end to end.

  • Largest file: 5 GB.
  • Longest file: 10 hours. A longer file is accepted, but only the first part up to that duration is transcribed, and the transcription has truncated set to true.
  • Transcriptions per day: No daily cap. A transcription counts when you complete the upload, not when you create the media record.
  • Fair use: 300 audio hours per month. Past that, jobs move to the standard queue. They are not rejected.
  • A limited number of uploads and jobs can be in progress at once. Your count and limit are in inFlight on Usage.
  • A pending upload is open for 24 hours after you create it. After that it is rejected with rejectReason set to upload_expired.
  • Presigned part URLs are valid for about one hour. The response has an expiresAt for each batch.

For the file types Scribewren accepts, see Uploading files.

POST /media

Registers a file and starts a multipart upload. Send a JSON body.

Field Type Required Notes
filename string Yes 1 to 255 characters.
sizeBytes integer Yes The exact size of the file in bytes, at least 1.
contentType string No Up to 255 characters. Defaults to application/octet-stream.
lastModified string or null No UTC ISO 8601 timestamp of the file.
durationMs integer or null No Duration reported by your client. It is untrusted and used only as a routing hint.
options object Yes See the next table. Send {} to take the defaults.

The options object takes these keys. Leave out a key to use its default.

Key Type Default Notes
language string auto auto detects the language. Otherwise use a transcription code from GET /languages. An unknown code is VALIDATION_ERROR with reason invalid_enum.
diarization boolean true Label who speaks when.
expectedSpeakers integer or null null A hint from 1 to 10. The current transcription backend may ignore it.

Unknown fields anywhere in the body are rejected.

Terminal window
curl -sS -X POST "https://api.scribewren.com/v1/media" \
-H "Authorization: Bearer $SCRIBEWREN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"interview.mp3","sizeBytes":734003200,"contentType":"audio/mpeg","options":{"language":"auto","diarization":true}}'

The response is 201 with a Media object. The upload object tells you how to split the file.

{
"success": true,
"code": 201,
"message": "Created",
"data": {
"id": "019a4f2d-e5a0-7b42-8c11-6d2f0e9a7b13",
"status": "pending_upload",
"filename": "interview.mp3",
"sizeBytes": 734003200,
"contentType": "audio/mpeg",
"lastModified": null,
"options": { "language": "auto", "diarization": true, "expectedSpeakers": null },
"upload": {
"uploadId": "EXAMPLE-UPLOAD-ID",
"partSizeBytes": 8388608,
"partCount": 88,
"expiresAt": "2026-12-02T09:41:07.512Z"
},
"sourceDurationMs": null,
"rejectReason": null,
"createdAt": "2026-12-01T09:41:07.512Z",
"uploadedAt": null,
"processedAt": null
},
"error": null,
"meta": { "requestId": "019a4f30-1c22-7a10-b5d3-42f1c0a8e777", "timestamp": "2026-12-01T09:41:07.512Z" }
}

Split the file into partCount parts of exactly partSizeBytes bytes each. The last part holds the remainder. partCount is at most 9000.

Error Status When
VALIDATION_ERROR 400 A field is missing, has the wrong type or is out of range, or the body has an unknown field.
FILE_TOO_LARGE 400 sizeBytes is above the plan’s maximum. error.details.maxBytes has the limit.
UNSUPPORTED_MEDIA_TYPE 400 The extension is not supported and contentType is not audio/* or video/*. error.details has extension and contentType.
UNSUPPORTED_MEDIA_TYPE 415 The request Content-Type is not application/json. The status tells the two cases apart.
PLAN_LIMIT_EXCEEDED 403 A plan limit blocks the upload, such as too many uploads and jobs in progress (details.limit is concurrency).
ACCOUNT_RESTRICTED 403 The account cannot start new processing.
SERVICE_UNAVAILABLE 503 New uploads are paused (details.reason is uploads_paused). Retry later.
RATE_LIMITED 429 See Rate limits.

POST /media/{id}/parts

Field Type Required Notes
partNumbers array of integers Yes 1 to 100 numbers, each from 1 to 9000.

The response lists one presigned URL per part and when the URLs expire.

{
"parts": [
{ "partNumber": 1, "url": "https://storage.example.com/EXAMPLE-PRESIGNED-URL-1" },
{ "partNumber": 2, "url": "https://storage.example.com/EXAMPLE-PRESIGNED-URL-2" }
],
"expiresAt": "2026-12-01T10:41:07.512Z"
}

This object is the data field of the usual envelope. Upload each part with an HTTP PUT to its URL. Send the raw bytes as the body, and no Authorization header: the signature is part of the URL. A successful response has an ETag header. Keep it exactly as returned, including any quotes, because complete needs it.

Presigned URLs are secrets for their lifetime. Do not log them or share them.

You can ask for URLs in batches of up to 100, or one at a time just before each upload. If a URL expires, ask for a new one for that part number.

The media must be in pending_upload. Otherwise the call returns 409 INVALID_STATE_TRANSITION with details.reason set to not_pending.

GET /media/{id}/parts

Returns the parts that storage already holds, so you can resume an interrupted upload.

{
"parts": [
{ "partNumber": 1, "etag": "\"EXAMPLEETAG1\"", "sizeBytes": 8388608 }
]
}

The same 409 applies when the media is not pending. If storage has already dropped the upload, details.reason is upload_expired.

POST /media/{id}/complete

Field Type Required Notes
parts array of objects Yes One { "partNumber", "etag" } object for every part, ordered from 1 to partCount.

The body can be up to 1 MiB. A larger body returns 413 PAYLOAD_TOO_LARGE.

On success the API assembles the file, checks that its size matches sizeBytes, creates the transcription and returns both objects with status 201.

{
"media": { "id": "019a4f2d-e5a0-7b42-8c11-6d2f0e9a7b13", "status": "uploaded" },
"transcription": { "id": "019a4f2e-7c1b-7d3a-9f00-3c5e8a1b2d4f", "status": "queued" }
}

The example shows only the main fields. media is a full Media object and transcription is a full Transcription object, as described in Transcriptions.

Completing is idempotent. If you call it again for a media that is already complete, you get 200 with the same result and no second transcription.

Error Status When
UPLOAD_INVALID 400 The parts do not match the upload. details.reason is part_count_mismatch, invalid_parts, missing_object or size_mismatch. After size_mismatch the media is rejected.
INVALID_STATE_TRANSITION 409 The media is no longer pending (details.reason is not_pending).
PLAN_LIMIT_EXCEEDED 403 A plan limit is reached, such as too many uploads and jobs open at once (details.limit is concurrency).
ACCOUNT_RESTRICTED 403 The account cannot start new processing.
PAYLOAD_TOO_LARGE 413 The body is over 1 MiB.
SERVICE_UNAVAILABLE 503 New uploads are paused.

DELETE /media/{id}

Aborts a pending upload and discards the parts. The response is 200 with data set to null. If the media is not pending, the call returns 409 INVALID_STATE_TRANSITION.

To delete a finished transcription, use DELETE /transcriptions/{id} instead. See Transcriptions.

GET /media?status=pending_upload

Returns up to 50 uploads that are still open, newest first, so a client can find an upload to resume. The status query parameter is required and pending_upload is the only accepted value. Anything else is VALIDATION_ERROR with reason invalid_enum.

Field Type Notes
id string UUID.
status string pending_upload, uploaded, processed or rejected.
filename string As you sent it. Only you can read it.
sizeBytes integer
contentType string Lower-cased.
lastModified string or null
options object language, diarization and expectedSpeakers.
upload object or null Set only while status is pending_upload. Has uploadId, partSizeBytes, partCount and expiresAt.
sourceDurationMs integer or null The full length of the media, once it is known.
rejectReason string or null Set when status is rejected. One of user_aborted, upload_expired, size_mismatch, unsupported_type, unreadable or no_audio_stream.
createdAt, uploadedAt, processedAt string or null UTC timestamps.

Can't find what you need? Contact support, email [email protected], or open Help and then Contact in the app. scribewren.com