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": {
"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
codefield is always a stable, lowercase string
Connect-RPC Error Codes
| Code | HTTP Status | Description |
|---|---|---|
invalid_argument | 400 | Request validation failed (bad input) |
not_found | 404 | Resource does not exist |
already_exists | 409 | Resource already exists (e.g., duplicate slug) |
permission_denied | 403 | Authenticated but not authorized for this action |
unauthenticated | 401 | Missing or invalid API key |
failed_precondition | 400 | Request cannot be fulfilled in current state |
resource_exhausted | 429 | Rate limit exceeded |
internal | 500 | Unexpected server error |
unavailable | 503 | Service temporarily unavailable |
deadline_exceeded | 504 | Request timed out |
unimplemented | 501 | Endpoint not yet implemented |
ErrorDetails
The details array contains one or more ErrorDetails objects with additional context:
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code (e.g., validation_error, parameter_out_of_range) |
message | string | Human-readable error description |
field_violations | FieldViolation[] | List of field-level errors (for validation failures) |
FieldViolation
Each field violation points to a specific field in the request:
| Field | Type | Description |
|---|---|---|
field | string | Dot-notation path to the invalid field |
description | string | What is wrong with the field |
Field paths use dot notation with array indices:
input_url— top-level fieldoutputs[0].type— first output’s type fieldoutputs[0].video[0].h264.crf— nested codec optionmetadata.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": {
"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 (usually due to a billing issue).
Solution: Contact support or resolve the billing issue to reactivate the organization.
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
- Always check the
codefield for programmatic error handling — do not parse themessagestring. - Handle all validation errors at once — the API returns every field violation in a single response.
- Retry on
unavailableandresource_exhausted— use exponential backoff with jitter. The official SDKs honorRetry-Afterand retry these automatically. - Do not retry on
invalid_argumentornot_found— these indicate a problem with your request. - Log the full error response — including
detailsandfield_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:
| Class | HTTP status | When |
|---|---|---|
APIConnectionError | — | Network / DNS / TLS failure |
APIError | 5xx | Server-side internal error |
AuthenticationError | 401 | Invalid, missing, or revoked API key |
PermissionError | 403 | Authenticated but lacking permission |
NotFoundError | 404 | Resource doesn’t exist |
ConflictError | 409 | Idempotency conflict, slug taken |
RateLimitError | 429 | Carries retryAfterMs / retry_after_ms / RetryAfter |
InvalidRequestError | 400 / 422 | Carries errors: FieldViolation[] |
PreconditionError | 412 | Wrong 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[]?.value.field_violations[]? | "(.field): (.description)"' /tmp/resp.json
fiimport {
TranscodelyError,
AuthenticationError,
NotFoundError,
RateLimitError,
InvalidRequestError,
} from "transcodely";
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, ¬Found):
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)
}
}
_ = jobDirect 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())
}
}
}