Appearance
Conventions & errors
The rules every endpoint shares. For the on-ramp read the API reference first; for a specific endpoint's schema see API operations.
IDs
| Prefix | Resource | Example |
|---|---|---|
job_ | A job (one utility run) | job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3 |
key_ | An API key (console only; the key itself looks like du_live_…) | key_01K7Z4A1B2C3D4E5F6G7H8J9K0 |
The part after the prefix is a ULID: 26 Crockford base-32 characters, lexicographically sortable by creation time. Treat IDs as opaque strings apart from the prefix.
API keys are du_live_ followed by 48 hex characters. The first 16 characters (du_live_3d53540f) are the key's prefix — safe to log and shown in the console to tell keys apart.
Timestamps
All timestamps are ISO-8601 in UTC with milliseconds: 2026-09-10T17:30:08.114Z. Fields that haven't happened yet are null (a queued job has startedAt: null).
Null vs. absent
Every documented field on a job is always present; "no value" is null (result: null on a failed job, callback: null without a callback URL). The optional file-reference fields are the exception: urlExpiresAt, pageNumber and path are omitted when they don't apply.
JSON only
Bodies are JSON (Content-Type: application/json). An empty body on a POST is read as {} — which then fails validation because input is required. Malformed JSON is 400 invalid_request ("Request body is not valid JSON").
Errors
The error envelope
Every request-level error — bad input, bad key, rate limit, unknown route — uses one shape:
json
{
"error": {
"code": "invalid_request",
"message": "input failed validation for html-pdf",
"details": [
{ "path": "input.url", "message": "must be an http://, https:// or s3:// URL" },
{ "path": "input.pdfOptions", "message": "use either format or width/height, not both" }
]
}
}codeis stable — branch on it, never onmessage.messageis human-readable and may change.detailsappears on validation errors: a list of{ path, message }, wherepathis dotted from the request body (input.files.2.fileName,metadata,(root)for the body as a whole).- On an unexpected
500, the error carries arequestId— quote it when reporting a problem; it matches the server logs.
Request errors vs. failed jobs
There are two kinds of failure, and they look different on purpose:
| Request error | Failed job | |
|---|---|---|
| When | The request is refused before anything runs: bad key, bad input, rate limit, unknown utility. | The utility ran (or tried to) and didn't succeed. |
| Job created? | No. | Yes — it has an id, a timeline, and shows up in the console and in usage. |
| Body | { "error": { code, message, details? } } | The job, with status: "failed" and error: { code, message, upstreamStatus? }. |
| HTTP status | From the code (table below). | Sync route: from error.code. GET /v1/jobs/{id}: always 200. |
upstreamStatus is the status the utility itself answered with, when the failure came from the utility — useful when reporting a problem with a specific input.
json
// A failed sync run → 422, body is the job
{
"id": "job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3",
"status": "failed",
"result": null,
"error": {
"code": "upstream_rejected",
"message": "Password-protected PDFs are not supported",
"upstreamStatus": 400
},
"…": "…"
}Error codes
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
invalid_request | 400 | The body, query or a header failed validation (see details). | No — fix the request. |
unauthorized | 401 | Missing, unknown, revoked or expired API key. | No — fix the key. |
forbidden | 403 | The key isn't allowed to call this utility. | No — use a key scoped to it. |
not_found | 404 | Unknown route, utility (or disabled on this deployment), or a job that isn't this key's. | No. |
conflict | 409 | Idempotency-Key reused for a different utility, or cancelling a job that isn't queued. | No. |
rate_limited | 429 | The key's per-minute budget is spent. | Yes — after Retry-After. |
upstream_rejected | 422 | The utility refused this input: couldn't download it, not a valid PDF, encrypted, too many pages… | No — the same input fails the same way. |
upstream_error | 502 | The utility failed or crashed. Transient upstream failures (throttling, 502/503) are already retried by the platform before you see this. | Maybe — once, later. |
upstream_timeout | 504 | The utility exceeded its time limit (900 s; 300 s for XLS → XLSX). | Only with a smaller input. |
storage_error | 502 | The utility succeeded but copying its output to storage failed. | Yes. |
lost | 500 | The job was interrupted (e.g. a restart) and ran past its time limit. | Yes. |
internal | 500 | Something unexpected on our side; includes requestId. | Yes, with backoff. |
Retrying safely
Send an Idempotency-Key on every run you might retry. A network timeout on your side then never produces two jobs — the retry returns the first one.
Headers
You send
| Header | Where | Purpose |
|---|---|---|
Authorization: Bearer du_live_… | every request | Your API key. |
Content-Type: application/json | requests with a body | Required for the body to be read as JSON. |
Idempotency-Key | POST /v1/utilities/{slug}[/jobs] | ≤ 200 chars. Replays return the original job. |
Prefer: respond-async | POST /v1/utilities/{slug} | Run as an async job instead. |
X-Request-Id | any (optional) | Your correlation id (≤ 64 chars), echoed back and written to server logs. |
You receive
| Header | Where | Purpose |
|---|---|---|
X-Request-Id | every response | Yours, echoed — or one we generated. |
RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset | authenticated responses | Your per-minute budget. |
Retry-After | 429, and a sync 202 | Seconds to wait before retrying / polling. |
Location | every 202 | The job URL (/v1/jobs/job_…). |
Idempotent-Replayed: true | a replayed idempotent request | This is the original job, not a new run. |
WWW-Authenticate | 401 | Why the key was refused. |
Metadata
metadata is a flat-or-nested JSON object you attach to a run: at most 50 top-level keys, key names ≤ 64 characters, and ≤ 4 KB once serialised. It's stored with the job and returned verbatim on the job and in its callback (list summaries omit it). It is never interpreted.
Limits at a glance
| Limit | Value |
|---|---|
Sync wait before 202 | 90 s |
?wait long-poll | ≤ 60 s |
GET /v1/jobs page size | 1–100 (default 25) |
Idempotency-Key | ≤ 200 chars |
callbackUrl | ≤ 2048 chars, https:// |
metadata | ≤ 50 keys, ≤ 4 KB |
Source URL (input.url) | ≤ 4096 chars |
| Default rate limit | 600 requests / minute / key |
| Job retention | 90 days by default (usage statistics are kept longer) |