Search Documentation
Search across all documentation pages
Errors

Errors

The Transcodely API uses Connect-RPC error codes with structured error details. Every error response includes a machine-readable code, a human-readable message, and — for validation errors — field-level details that pinpoint exactly what went wrong.

Error Response Format

All error responses follow this structure:

{
  "code": "invalid_argument",
  "message": "Request validation failed",
  "details": [
    {
      "type": "transcodely.v1.ErrorDetails",
      "value": "CgtpbnZhbGlkX2FyZ2NvZGVjX2lzX3JlcXVpcmVk",
      "debug": {
        "code": "validation_error",
        "message": "Request validation failed",
        "field_violations": [
          {
            "field": "outputs[0].video[0].codec",
            "description": "codec is required"
          },
          {
            "field": "input_url",
            "description": "must match pattern: ^(gs|s3|https?)://.*$"
          }
        ]
      }
    }
  ]
}

Key design decisions:

  • All validation errors are returned at once — the API does not stop at the first error
  • Field paths include array indices — e.g., outputs[0].video[0].h264.crf
  • Errors are machine-readable — the code field is always a stable, lowercase string

Connect-RPC Error Codes

CodeHTTP StatusDescription
invalid_argument400Request validation failed (bad input)
not_found404Resource does not exist
already_exists409Resource already exists (e.g., duplicate slug)
permission_denied403Authenticated but not authorized for this action
unauthenticated401Missing or invalid API key
failed_precondition412Request cannot be fulfilled in current state
resource_exhausted429Rate limit exceeded
internal500Unexpected server error
unavailable503Service temporarily unavailable
deadline_exceeded504Request timed out
unimplemented501Endpoint not yet implemented

Error code catalog

Alongside the Connect code, most errors carry a stable error_code slug — returned as an error-code header on the error metadata and, for the errors that also fail a job, as the job’s own error_code field. Switch on the slug, not on the message.

These are the codes your plan’s limits can produce:

Connect codeerror_code slugCause
resource_exhaustedlimit_exceededThe app reached its monthly spend limit. Clears when the next billing period starts or the limit is raised.
resource_exhaustedqueue_limit_exceededThe app’s queue is at its ceiling. Clears as queued jobs drain — retry with backoff.
invalid_argumentoutput_duration_limit_exceededThe job’s effective encode duration exceeds the plan’s output duration cap. Also appears as a job error_code when the source duration is only learned at probe time; the job fails and a job.failed webhook fires. The cap applies to what would be encoded — a clip range within the cap is accepted regardless of source length.
invalid_argumentquality_tier_not_allowedAn output (or ABR variant) requests a quality tier outside the plan’s allowed set. Always rejected at create time. Omitted tiers default to standard, which may itself be outside the set.

The two resource_exhausted codes are worth retrying, because the condition behind them clears on its own. The two invalid_argument codes are not: the request as written is over a plan limit and will be over it again. Change the request, or talk to us about a plan change.

Billing and balance codes

These codes are about money rather than capacity. None of them clears on its own — no queue drains and no billing period resets them — so retrying on a backoff asks a question whose answer cannot change. Each one names a remedy instead.

Connect codeerror_code slugCauseClears when
failed_preconditionbilling_past_dueA statement issued to your organization is unpaid and has reached day 7 of the payment-failure schedule. New jobs are refused; queued and running work is unaffected.The statement is paid
failed_preconditionoutstanding_balance_exceededUsage that has not been invoiced yet reached twice your organization’s balance threshold. New jobs are refused; queued and running work, playback, uploads and cancellation are unaffected.The balance is paid
failed_preconditionsettlement_unavailableYou asked to pay your outstanding balance mid-cycle on a deployment where that rail is switched off. Nothing about your account is the problem.An operator enables it
failed_preconditionnothing_outstandingYou asked to pay an outstanding balance and there is nothing to pay — the everyday result of pressing Pay twice.More usage accrues

When an unpaid statement and an over-threshold balance both apply, you get billing_past_due. The statement is the more concrete thing to act on — a real number, an existing Pay action, and a deadline the other does not carry — and you hear about the balance on the very next create once the statement is settled.

ErrorDetails

Each entry in the details array wraps an ErrorDetails message. The entry’s value holds the base64-encoded binary Protobuf of that message, and a sibling debug field carries the same message projected as readable JSON. The code, message, and field_violations fields shown above live under debug — read them there, not under value. The ErrorDetails fields are:

FieldTypeDescription
codestringMachine-readable error code (e.g., validation_error, parameter_out_of_range)
messagestringHuman-readable error description
field_violationsFieldViolation[]List of field-level errors (for validation failures)

FieldViolation

Each field violation points to a specific field in the request:

FieldTypeDescription
fieldstringDot-notation path to the invalid field
descriptionstringWhat is wrong with the field

Field paths use dot notation with array indices:

  • input_url — top-level field
  • outputs[0].type — first output’s type field
  • outputs[0].video[0].h264.crf — nested codec option
  • metadata.my_key — metadata entry

Common Error Scenarios

Invalid API Key

{
  "code": "unauthenticated",
  "message": "Invalid or missing API key"
}

Cause: The Authorization header is missing, the key is malformed, the key has been revoked, or the key has expired.

Solution: Check that you are passing a valid API key as Bearer {{API_KEY}} in the Authorization header.

Resource Not Found

{
  "code": "not_found",
  "message": "Job not found: job_nonexistent123"
}

Cause: The resource ID does not exist, or it belongs to a different app.

Solution: Verify the resource ID and ensure you are using the correct API key (keys are scoped to an app).

Validation Error

{
  "code": "invalid_argument",
  "message": "Request validation failed",
  "details": [
    {
      "type": "transcodely.v1.ErrorDetails",
      "value": "Cg92YWxpZGF0aW9uX2Vycm9yEhpDUkYgbXVzdCBiZSBiZXR3ZWVu",
      "debug": {
        "code": "validation_error",
        "message": "Request validation failed",
        "field_violations": [
          {
            "field": "outputs[0].video[0].h264.crf",
            "description": "CRF must be between 15 and 35"
          }
        ]
      }
    }
  ]
}

Cause: One or more request fields failed validation.

Solution: Read each field_violation to identify and fix the invalid fields. All violations are returned at once so you can fix them in a single pass.

Organization Suspended

{
  "code": "permission_denied",
  "message": "Organization is suspended"
}

Cause: The organization associated with the API key is suspended. The most common reason is an invoice left unpaid for two weeks — see the payment-failure schedule — but an organization can also be suspended deliberately by a person.

Solution: Sign in to the dashboard, which stays readable while an organization is suspended, and settle anything outstanding. Paying lifts a suspension applied for non-payment on the next request. A suspension applied for any other reason is not lifted by paying; contact support.

Quality Tier Not Allowed

{
  "code": "invalid_argument",
  "message": "app app_k1l2m3n4o5 may only use quality tiers [economy] and output 0 requests "premium" (set by the Free plan); request an allowed tier or contact support about a plan change"
}

Cause: An output — or any variant of an ABR ladder — asked for a quality tier that your plan does not allow. This is always caught at create time, so no job is created and nothing is billed. Note that an output which does not name a tier is a request for standard, not an exemption: on a plan that allows only economy, a bare output is rejected too.

Solution: Request a tier your plan allows, naming it explicitly on every output and every variant. If your workload genuinely needs the tier, contact support about a plan change.

Output Duration Limit Exceeded

{
  "code": "invalid_argument",
  "message": "app app_k1l2m3n4o5 is limited to output durations of at most 30s and this job would encode 142.0s (set by the Free plan); shorten the clip range or contact support about a plan change"
}

Cause: The job would encode more video than your plan’s output duration cap allows. The cap measures the effective encode window — what would actually be encoded — not the raw length of the source, so a clip range inside the cap is fine on a source of any length.

This limit is enforced in two places, because a source’s length is not always knowable up front. When it is (an explicit clip range, or a source we already have the duration for) the request is rejected outright, as above. When it is not — a URL or origin source with no clip range — the job is accepted and the check runs once the source has been probed. In that case the job moves to failed with error_code output_duration_limit_exceeded and a job.failed webhook fires; read the message on the job for the numbers.

Solution: Add or shorten a clip range so the encode window fits inside the cap, or contact support about a plan change.

Duplicate Resource

{
  "code": "already_exists",
  "message": "Preset with slug 'web_720p_fast' already exists"
}

Cause: Attempting to create a resource with a slug or identifier that is already in use.

Solution: Use a different slug, or retrieve the existing resource.

State Conflict

{
  "code": "failed_precondition",
  "message": "Job is not in a cancelable state: completed"
}

Cause: The requested operation is not valid for the resource’s current state (e.g., canceling a completed job, confirming a job that is not awaiting confirmation).

Solution: Check the resource’s current status before making state-transition requests.

Error Handling Best Practices

  1. Always check the code field for programmatic error handling — do not parse the message string.
  2. Handle all validation errors at once — the API returns every field violation in a single response.
  3. Retry on unavailable and resource_exhausted — use exponential backoff with jitter. The official SDKs honor Retry-After and retry these automatically.
  4. Do not retry on invalid_argument or not_found — these indicate a problem with your request.
  5. Do not retry the billing codesbilling_past_due and outstanding_balance_exceeded clear when somebody pays, not when time passes. Surface them to a human instead of backing off.
  6. Log the full error response — including details and field_violations — for debugging.

SDK error handling

If you use one of the official SDKs, prefer the typed error classes — they give you instanceof / isinstance / errors.As-style matching without parsing strings.

Every error inherits from a base TranscodelyError and exposes the same fields across all three SDKs:

ClassHTTP statusWhen
APIConnectionErrorNetwork / DNS / TLS failure
APIError5xxServer-side internal error
AuthenticationError401Invalid, missing, or revoked API key
PermissionError403Authenticated but lacking permission
NotFoundError404Resource doesn’t exist
ConflictError409Idempotency conflict, slug taken
RateLimitError429Carries retryAfterMs / retry_after_ms / RetryAfter
InvalidRequestError400 / 422Carries errors: FieldViolation[]
PreconditionError412Wrong state (e.g. job not cancelable)

Every error carries code, httpStatus, requestId, and raw (the original response body) for debugging.

Without an SDK there are no typed exceptions to catch — instead, inspect the HTTP status and read the structured code and field_violations straight out of the JSON body (e.g. with jq):

# Capture the response body and the HTTP status separately (no -f, so the
# error body is still written to /tmp/resp.json on a 4xx/5xx).
http_status=$(curl -sS -o /tmp/resp.json -w '%{http_code}' 
  -X POST "https://api.transcodely.com/transcodely.v1.JobService/Create" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "Content-Type: application/json" 
  -d '{"input_url": "not-a-url", "outputs": []}')

if [ "$http_status" -ge 400 ]; then
  # Top-level Connect code (e.g. "invalid_argument", "not_found").
  jq -r '.code, .message' /tmp/resp.json
  # Field-level violations, when present (HTTP 400/422).
  jq -r '.details[]?.debug.field_violations[]? | "(.field): (.description)"' /tmp/resp.json
fi
import {
  TranscodelyError,
  AuthenticationError,
  NotFoundError,
  RateLimitError,
  InvalidRequestError,
} from "@transcodely/sdk";

try {
  await client.jobs.create(request);
} catch (err) {
  if (err instanceof InvalidRequestError) {
    for (const v of err.errors) {
      console.error(`${v.field}: ${v.description}`);
    }
  } else if (err instanceof RateLimitError) {
    await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000));
  } else if (err instanceof NotFoundError) {
    console.error("not found:", err.message);
  } else if (err instanceof AuthenticationError) {
    console.error("bad API key");
  } else if (err instanceof TranscodelyError) {
    console.error(`[${err.requestId}] ${err.code}: ${err.message}`);
  } else {
    throw err;
  }
}
import time
from transcodely import (
    TranscodelyError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    InvalidRequestError,
)

try:
    client.jobs.create(input_url=..., outputs=[...])
except InvalidRequestError as err:
    for v in err.errors:
        print(f"{v.field}: {v.description}")
except RateLimitError as err:
    time.sleep((err.retry_after_ms or 1000) / 1000)
except NotFoundError as err:
    print(f"not found: {err}")
except AuthenticationError:
    print("bad API key")
except TranscodelyError as err:
    print(f"[{err.request_id}] {err.code}: {err}")
import (
    "errors"
    "time"

    "github.com/transcodely/transcodely-go"
)

job, err := client.Jobs.Create(ctx, params)
if err != nil {
    var notFound *transcodely.NotFoundError
    var invalid  *transcodely.InvalidRequestError
    var rate     *transcodely.RateLimitError
    var auth     *transcodely.AuthenticationError

    switch {
    case errors.As(err, &notFound):
        log.Printf("not found, request_id=%s", notFound.RequestID())
    case errors.As(err, &invalid):
        for _, v := range invalid.Errors() {
            log.Printf("%s: %s", v.Field, v.Description)
        }
    case errors.As(err, &rate):
        time.Sleep(rate.RetryAfter)
    case errors.As(err, &auth):
        log.Fatal("bad API key")
    default:
        log.Fatal(err)
    }
}
_ = job

Direct Connect-RPC error handling

If you call the API without an SDK (e.g. from a generated Connect-ES or connect-go stub), match on the Connect Code directly. The SDK examples above are equivalent — they wrap this code-based matching in typed classes.

There is no public Connect-RPC client library for Python — the official Python SDK speaks Connect over plain HTTP/JSON internally, and there is no low-level ConnectError to catch. If you are not using the SDK from Python, inspect the HTTP status and JSON body directly (see the curl + jq snippet under SDK error handling); otherwise use the SDK’s typed errors shown below.

import { ConnectError, Code } from '@connectrpc/connect';

try {
  const job = await client.create(request);
} catch (err) {
  if (err instanceof ConnectError) {
    switch (err.code) {
      case Code.InvalidArgument:
        // Parse field violations from err.details
        console.error('Validation failed:', err.message);
        break;
      case Code.NotFound:
        console.error('Resource not found');
        break;
      case Code.Unauthenticated:
        // Redirect to login or refresh credentials
        break;
      case Code.Unavailable:
        // Retry with exponential backoff
        break;
      default:
        console.error('Unexpected error:', err.code, err.message);
    }
  }
}
# Python has no low-level Connect client — use the SDK's typed errors instead.
from transcodely import (
    InvalidRequestError,
    NotFoundError,
    AuthenticationError,
    APIError,
    TranscodelyError,
)

try:
    job = client.jobs.create(input_url=..., outputs=[...])
except InvalidRequestError as err:
    # Field violations are already parsed for you.
    for v in err.errors:
        print(f"validation failed: {v.field}: {v.description}")
except NotFoundError:
    print("resource not found")
except AuthenticationError:
    # Refresh credentials.
    ...
except APIError:
    # 5xx / unavailable — retry with exponential backoff.
    ...
except TranscodelyError as err:
    print(f"unexpected error: {err.code}: {err}")
import (
	"errors"

	"connectrpc.com/connect"
)

// client is a generated connect-go stub, e.g. from
// transcodelyv1connect.NewJobServiceClient(httpClient, "https://api.transcodely.com").
_, err := client.Create(ctx, connect.NewRequest(req))
if err != nil {
	var connectErr *connect.Error
	if errors.As(err, &connectErr) {
		switch connectErr.Code() {
		case connect.CodeInvalidArgument:
			// Field violations live in connectErr.Details().
			log.Printf("validation failed: %v", connectErr.Message())
		case connect.CodeNotFound:
			log.Print("resource not found")
		case connect.CodeUnauthenticated:
			// Refresh credentials.
		case connect.CodeUnavailable:
			// Retry with exponential backoff.
		default:
			log.Printf("unexpected error: %v: %v", connectErr.Code(), connectErr.Message())
		}
	}
}