Skip to content

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

PrefixResourceExample
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" }
    ]
  }
}
  • code is stable — branch on it, never on message.
  • message is human-readable and may change.
  • details appears on validation errors: a list of { path, message }, where path is dotted from the request body (input.files.2.fileName, metadata, (root) for the body as a whole).
  • On an unexpected 500, the error carries a requestId — 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 errorFailed job
WhenThe 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 statusFrom 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

CodeHTTPMeaningRetry?
invalid_request400The body, query or a header failed validation (see details).No — fix the request.
unauthorized401Missing, unknown, revoked or expired API key.No — fix the key.
forbidden403The key isn't allowed to call this utility.No — use a key scoped to it.
not_found404Unknown route, utility (or disabled on this deployment), or a job that isn't this key's.No.
conflict409Idempotency-Key reused for a different utility, or cancelling a job that isn't queued.No.
rate_limited429The key's per-minute budget is spent.Yes — after Retry-After.
upstream_rejected422The 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_error502The utility failed or crashed. Transient upstream failures (throttling, 502/503) are already retried by the platform before you see this.Maybe — once, later.
upstream_timeout504The utility exceeded its time limit (900 s; 300 s for XLS → XLSX).Only with a smaller input.
storage_error502The utility succeeded but copying its output to storage failed.Yes.
lost500The job was interrupted (e.g. a restart) and ran past its time limit.Yes.
internal500Something 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

HeaderWherePurpose
Authorization: Bearer du_live_…every requestYour API key.
Content-Type: application/jsonrequests with a bodyRequired for the body to be read as JSON.
Idempotency-KeyPOST /v1/utilities/{slug}[/jobs]≤ 200 chars. Replays return the original job.
Prefer: respond-asyncPOST /v1/utilities/{slug}Run as an async job instead.
X-Request-Idany (optional)Your correlation id (≤ 64 chars), echoed back and written to server logs.

You receive

HeaderWherePurpose
X-Request-Idevery responseYours, echoed — or one we generated.
RateLimit-Limit / RateLimit-Remaining / RateLimit-Resetauthenticated responsesYour per-minute budget.
Retry-After429, and a sync 202Seconds to wait before retrying / polling.
Locationevery 202The job URL (/v1/jobs/job_…).
Idempotent-Replayed: truea replayed idempotent requestThis is the original job, not a new run.
WWW-Authenticate401Why 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

LimitValue
Sync wait before 20290 s
?wait long-poll≤ 60 s
GET /v1/jobs page size1–100 (default 25)
Idempotency-Key≤ 200 chars
callbackUrl≤ 2048 chars, https://
metadata≤ 50 keys, ≤ 4 KB
Source URL (input.url)≤ 4096 chars
Default rate limit600 requests / minute / key
Job retention90 days by default (usage statistics are kept longer)

ServiceLabs · a Finnoto company