Skip to main content
Every Roughy API endpoint reports failures with the same response shape, regardless of which endpoint failed or which HTTP status code is returned. Successful responses are the resource directly (no wrapper); error responses are always the envelope below.

Envelope

{
  "error": {
    "code": "validation_error",
    "message": "Upload-Length must be at least 64 bytes.",
    "fields": [
      { "field": "body.name", "message": "Field required" }
    ]
  }
}
FieldTypeWhen setUse it for
codestring enumalwaysBranching client logic. Switch on this, not on message.
messagestringalwaysShowing the user a single-line description. Safe to surface verbatim.
fieldsarrayonly when code is validation_error and one or more request fields failedHighlighting bad form fields. field is a dotted path rooted at the request location (body.…, query.…, path.…).

Error codes

codeTypical statusWhat it means
bad_request400Malformed request that isn’t covered by a more specific code.
unauthenticated401Missing or invalid Authorization: Bearer sk_….
insufficient_credits402Your credit balance is at or below zero, so POST /v1/assets rejects the upload before it starts — top up first. An upload that starts with a positive balance but can’t cover the final per-minute charge instead lands the asset in pending_payment, which activates on your next top-up.
forbidden403Authenticated, but not allowed for this resource. Rare — most ownership failures surface as not_found so an attacker can’t probe existence.
not_found404Resource doesn’t exist, or doesn’t belong to you.
conflict409A state-machine violation not covered by one of the more specific 409 codes below.
asset_processing409POST /v1/cuts: the asset is still being prepared and isn’t cuttable yet. Transient — back off for the Retry-After seconds and retry.
asset_payment_required409POST /v1/cuts: the asset is prepared but unpaid (pending_payment). Top up, then retry.
asset_failed409POST /v1/cuts: the asset failed preparation and can’t be cut. Terminal — re-upload.
asset_not_ready409POST /v1/renders: a video output needs the asset ready (an audio output instead gates on cuttable, like a cut).
modifier_not_ready409POST /v1/renders: the cut_id names a cut of this asset that isn’t completed yet.
unsupported_format409A cut/subtitle export format that isn’t available for this entity.
precondition_failed412A required header / preflight condition wasn’t met (e.g. Tus-Resumable).
payload_too_large413Body / Upload-Length exceeds the documented cap.
unsupported_media_type415Wrong Content-Type on the request body.
validation_error422The request was well-formed but one or more fields failed validation. fields[] lists each one.
internal_error500Something failed server-side. Include the X-Request-Id header in a support ticket so we can trace the request.
upstream_error502A downstream service rejected our integration. Retry won’t help without changing the request.
upstream_unavailable503A downstream service is transiently unreachable. Retry after the Retry-After header’s seconds.
Only the transient codes carry a Retry-After header: asset_processing (on POST /v1/cuts, while the asset is still being prepared) and upstream_unavailable. Back off for the header’s seconds and retry the same request. The list is closed — Roughy won’t introduce a new code without a backward-compatible documentation update. If you encounter a code not listed here, treat it as internal_error.

Correlating with logs

Every response (success or error) carries an X-Request-Id response header. Our server-side structured logs bind the same id to every line emitted while processing the request, so including the header value in a support ticket lets us trace the chain. The id is header-only — the error envelope body does not echo it. When opening a ticket, grab the X-Request-Id from the response (visible in your browser’s network panel or curl -v) and paste it alongside the JSON.

Example — validation_error with fields

POST /v1/projects HTTP/1.1
Authorization: Bearer sk_live_…
Content-Type: application/json

{ "name": "" }
HTTP/1.1 422 Unprocessable Content
X-Request-Id: 01HZ6P7K9V3QMZB2ACS9YYCP9N
Content-Type: application/json

{
  "error": {
    "code": "validation_error",
    "message": "String should have at least 1 character",
    "fields": [
      { "field": "body.name", "message": "String should have at least 1 character" }
    ]
  }
}

Example — not_found

GET /v1/projects/00000000-0000-0000-0000-000000000000 HTTP/1.1
Authorization: Bearer sk_live_…
HTTP/1.1 404 Not Found
X-Request-Id: 01HZ6P7K9V3QMZB2ACS9YYCP9N
Content-Type: application/json

{
  "error": {
    "code": "not_found",
    "message": "Project 00000000-0000-0000-0000-000000000000 not found"
  }
}
Note fields is absent — it’s only present for validation errors.

Success responses

For symmetry, the success side: status codes carry the meaning, the body is the resource directly. No wrapper.
StatusBody
200 OKThe resource.
201 CreatedThe newly-created resource, plus a Location header.
202 AcceptedReference identifiers for async work.
204 No ContentEmpty body (per RFC 9110 §15.3.5).
Listings carry pagination metadata — { items: [...], total, limit, offset }. That’s a pagination shape, not a generic envelope.