Public API · 2026-09-01

Document processing, metered per page.

Upload a file, start a job, get the result — OCR with real coordinates, conversion, compression, merge/split, page operations, protection and redaction. Every job runs on the same workers as the editor; nothing is faked. Bearer API keys, JSON everywhere, signed webhooks, a free sandbox.

€19/monthincl. VAT2,000 pagesincluded / month€0.015per extra credit (OCR: 3/page)https://docaxo.com/api/v1

Quickstart

Create a key under Account → API keys (pick Sandbox for a dxk_test_ key that never bills, or Live for dxk_live_). Then: upload, create a job, poll, download. Three calls.

# 1. Upload
curl -s https://docaxo.com/api/v1/files \
  -H "Authorization: Bearer $DOCAXO_API_KEY" \
  -F "[email protected]"
# → {"id":"f_…","object":"file","page_count":3,"status":"ready",…}

# 2. Start a job (safe to retry thanks to Idempotency-Key)
curl -s https://docaxo.com/api/v1/jobs \
  -H "Authorization: Bearer $DOCAXO_API_KEY" \
  -H "Idempotency-Key: order-42-ocr" \
  -H "Content-Type: application/json" \
  -d "{\"operation\":\"ocr\",\"file_id\":\"$FILE_ID\",\"options\":{\"mode\":\"base\"}}"
# → 202 {"id":"…","status":"queued","credits":{"billed":true,"pages_estimated":3},…}

# 3. Poll, then download
curl -s "https://docaxo.com/api/v1/jobs/$JOB_ID" -H "Authorization: Bearer $DOCAXO_API_KEY"
curl -sL "https://docaxo.com/api/v1/jobs/$JOB_ID/result?format=json" \
  -H "Authorization: Bearer $DOCAXO_API_KEY" -o blocks.json

Prefer push over polling? Register a webhook and wait for job.succeeded. The OpenAPI document lives at /api/v1/openapi.json.

Authentication

Send your key as a Bearer token: Authorization: Bearer dxk_live_…. Browser cookies are never accepted on /api/v1; a request without a key gets 401 authentication_required.

  • Prefix decides the mode. dxk_live_ keys process and bill; dxk_test_ keys use the sandbox. Files, jobs and webhooks are partitioned per mode — a live key cannot see sandbox objects (403 mode_mismatch).
  • Plan. Live keys need the API Starter plan, otherwise 403 plan_required with an upgrade_url. Sandbox keys work on every plan.
  • Scopes. documents:read, documents:write, jobs:read, jobs:write, usage:read, webhooks:manage. A missing scope is 403 insufficient_scope.
  • Rotation. Rotate from the account page; the old key keeps working for the grace period you choose. GET /api/v1/account tells you which key, plan and limits a request is running under.

Files

POST /api/v1/files accepts a multipart/form-data body with a single file part (PDF, DOCX, XLSX, CSV, JPEG, PNG, WebP; the bytes must match the declared type). The response includes page_count — that is what a job will reserve credits for — and the file is ready immediately.

For large uploads send JSON {filename, content_type, size_bytes} instead: you get a presigned PUT URL; upload the bytes, then call POST /files/{id}/complete. Presigned URLs need the S3 object store — on a local MinIO-less setup the API answers 501 presigned_unavailable, so fall back to multipart.

Files are inputs, not storage. Live files expire after 24h (expires_at), sandbox files too; a file.expired webhook fires when the sweeper removes one. Delete earlier with DELETE /files/{id}.

Jobs & operations

POST /api/v1/jobs with {operation, file_id | file_ids, options, metadata} returns 202 and a job in queued. Poll GET /jobs/{id} until succeeded, failed or cancelled, then fetch GET /jobs/{id}/result (format=download streams the file, json returns OCR blocks, url gives a short-lived presigned link). metadata (≤ 20 string keys) is echoed back on the job and in webhooks.

operationjob kindWhat it doesoptionscredits
ocrocrRecognise text — layout blocks with coordinates; optional searchable PDF.mode: gundam (single page) | base (multi-page), output: blocks | searchable_pdf, pages[] (0-based)per page
searchable_pdfocrOCR plus an invisible text layer → searchable PDF.mode, pages[]per page
convertconvertpdf→docx, pdf→jpg, docx→pdf, xlsx→csv, csv→xlsx. pptx / png / html / txt targets answer operation_unsupported.target: docx | jpg | pdf | csv | xlsx, dpi, quality, pages[], sheet, delimiter, sheet_name, has_headerper page
compresscompressReduce PDF size.level: light | balanced | strongper page
mergemergeMerge 2–50 PDFs in the given order (file_ids).free
splitsplitSplit by ranges, every page, or fixed chunks.mode: every_page (default) | ranges | chunks, ranges [[1,3],[4,4]] (1-based), chunk_sizefree
rotatepage_opsRotate pages clockwise.degrees: 90 | 180 | 270, pages[] (0-based, default all)free
reorderpage_opsReorder pages.order[] — full permutation of 0-based indexesfree
delete_pagespage_opsDelete pages.pages[] (0-based)free
protectprotectEncrypt with passwords and permissions.user_password, owner_password, allow_print, allow_modify, allow_copy, allow_annotatefree
unlockunlockRemove encryption (password required when set).passwordfree
redactredactSecure redaction — affected pages are rasterised and rebuilt.regions[] {page, x, y, w, h}, dpiper page
watermark422 operation_unsupportedNo watermark handler exists in the document worker yet; the browser editor stamps watermarks client-side.
flatten422 operation_unsupportedNo flatten handler exists in the document worker yet; exports are flattened client-side.
sign422 operation_unsupportedSigning goes through the share / signature-invitation flow and is not exposed to API keys.

Operations map one-to-one onto real worker job kinds. Anything without a handler — including convert targets pptx, png, html, txt — answers 422 operation_unsupported with a reason; we never return a fake result. GET /api/v1/operations serves this table as JSON.

POST /jobs/{id}/cancel stops a queued job immediately (its reservation is released) and asks a running one to stop at the next checkpoint. Per-job wall-clock timeouts and page caps (page_limit_exceeded) follow your plan; see GET /api/v1/account.

Idempotency

Network failures happen between “request sent” and “response read”. Send an Idempotency-Key header on POST /api/v1/jobs and retries are safe: the stored response is replayed with Idempotent-Replayed: true and no second job or reservation is created. Keys are scoped to the API key and kept for 24 hours.

POST /api/v1/jobs
Idempotency-Key: order-42-ocr        # ≤ 255 chars, scoped to the API key, kept 24h

# first call  → 202 + job
# retry       → 202 + the *same* job, header Idempotent-Replayed: true
# same key, different body → 422 idempotency_key_reused
# key still in flight      → 409 idempotency_in_progress

Webhooks

Register up to 10 HTTPS endpoints per mode with POST /api/v1/webhooks {url, events?, description?}. The response contains the signing secret (whsec_…) exactly once. POST /webhooks/{id}/test sends a signed webhook.test synchronously so you can wire things up before the first real job.

eventWhen
job.succeededA job finished and its result is downloadable.
job.failedA job failed; `data.object.error.code` explains why. Reserved credits are released.
job.cancelledA job was cancelled via POST /jobs/{id}/cancel.
file.expiredAn uploaded file reached its TTL and was deleted.
webhook.testSent synchronously by POST /webhooks/{id}/test.
Delivery
POST https://example.com/docaxo
Docaxo-Event: job.succeeded
Docaxo-Signature: t=1757700000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Content-Type: application/json

{
  "id": "evt_01J7…",
  "object": "event",
  "type": "job.succeeded",
  "api_version": "2026-09-01",
  "livemode": true,
  "created_at": "2026-09-12T18:00:00+00:00",
  "data": {
    "object": {
      "id": "…", "object": "job", "operation": "ocr", "status": "succeeded",
      "metadata": { "order": "42" },
      "result": { "download_url": "https://docaxo.com/api/v1/jobs/…/result", "pages_processed": 3 }
    }
  }
}

Verify the signature

Docaxo-Signature is t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, `${t}.${raw body}`). Compute it over the raw bytes (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyDocaxoSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

// Express: app.post("/docaxo", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyDocaxoSignature(req.body.toString("utf8"), req.get("Docaxo-Signature"), process.env.WHSEC)) return res.sendStatus(400);
//   const event = JSON.parse(req.body); … ; res.sendStatus(200);
// });

Retries

Any non-2xx response or a timeout (10 s) is retried with backoff: 1m → 5m → 30m → 2h → 12h — six attempts in total. Event ids are stable across retries, so deduplicate on id. After 30 consecutive failures an endpoint is disabled (disabled_reason); re-enable it with PATCH /webhooks/{id} {enabled: true}. Every attempt is visible at GET /webhooks/{id}/deliveries.

Rate limits

Each key has a token bucket of 60 requests per minute (sandbox: 30) and may have 2 jobs queued or running at once (sandbox: 2). Exceeding either answers 429 with rate_limited or concurrency_limit; back off for Retry-After seconds. Limits are per key, so give each integration its own. The concurrency figure is part of the API Starter plan; the effective limits for your key are always returned by GET /accountlimits.

HTTP/1.1 429 Too Many Requests
Retry-After: 3
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 3

{"error":{"type":"rate_limit_error","code":"rate_limited",…}}
HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetSeconds until the bucket refills
Retry-AfterOnly on 429 — seconds to wait
X-Request-IdEvery response; quote it in support requests
Idempotent-Replayed`true` when a stored idempotent response was returned
X-Docaxo-Sandbox`true` on sandbox result downloads

Errors

Every error is JSON with one envelope. type groups codes for coarse handling, code is stable and machine-readable, doc_url deep-links into this page and request_id matches the X-Request-Id header. Validation errors add param; billing errors add pages/remaining.

HTTP/1.1 422 Unprocessable Entity
X-Request-Id: req_5f1c…

{
  "error": {
    "type": "invalid_request_error",
    "code": "operation_unsupported",
    "message": "Operation 'watermark' is not available through the API",
    "doc_url": "https://docaxo.com/api-docs#error-operation_unsupported",
    "request_id": "req_5f1c…",
    "param": "operation"
  }
}
codestatustypeMeaning
authentication_required401authentication_errorNo Bearer API key on the request. Cookies are never accepted here.
invalid_api_key401authentication_errorUnknown, revoked or expired key.
plan_required403permission_errorLive keys need the API Starter plan. `upgrade_url` points to checkout.
insufficient_scope403permission_errorThe key lacks the scope this route needs (`jobs:write`, `webhooks:manage`, …).
mode_mismatch403permission_errorThe object belongs to the other mode (live ↔ test).
validation_error422invalid_request_errorMalformed body or parameters; `param` names the field.
operation_unsupported422invalid_request_errorOperation (or convert target) has no real worker handler. Never faked.
invalid_options422invalid_request_errorOptions do not validate for the chosen operation.
unsupported_file_type415invalid_request_errorContent type not accepted or does not match the bytes.
file_too_large413invalid_request_errorUpload exceeds the maximum size.
file_not_ready409conflict_errorPresigned upload has not been completed yet.
page_limit_exceeded422invalid_request_errorMore pages than the plan allows per job.
insufficient_credits402billing_errorNot enough page credits and overage is not available.
spending_cap_reached402billing_errorThe job would push overage past your spending cap.
concurrency_limit429rate_limit_errorToo many queued/running jobs for this key. Retry after one finishes.
rate_limited429rate_limit_errorToken bucket empty; honour `Retry-After`.
idempotency_key_reused422idempotency_errorSame Idempotency-Key, different request body.
idempotency_in_progress409conflict_errorThe original request with this key is still running.
not_found404not_found_errorNo such file, job or webhook for this account and mode.
job_not_finished409conflict_errorResult requested before the job succeeded.
no_downloadable_result409conflict_errorThe job produced no downloadable output.
result_expired404not_found_errorResult object was swept after its TTL.
presigned_unavailable501invalid_request_errorPresigned URLs need the S3 store; use multipart upload / streamed download.
webhook_delivery_failed502api_errorYour endpoint rejected the test event (attempt kept in the delivery log).
queue_unavailable503api_errorJob queue unreachable; safe to retry with the same Idempotency-Key.
internal_error500api_errorOur fault. Quote `request_id` when reporting.

Retry 429, 503 and network errors with exponential backoff and the same Idempotency-Key. Do not retry 4xx codes without changing the request.

Pricing & credits

API Starter

€19/monthincl. VAT

2,000 processing page credits every month, then €0.015 per credit (€0.045 per OCR page), metered through Stripe (docaxo_api_processing_page) and added to your monthly invoice. EU VAT / reverse charge is handled by Stripe Tax.

Subscribe on the pricing page →

How credits are counted

  • One credit = one page of a metered operation (convert, compress, redact). GPU OCR is the exception: ocr and searchable_pdf cost 3 credits per page. Merge, split, rotate, reorder, delete pages, protect and unlock are free.
  • Credits are reserved when a job is accepted (using the file’s page count) and settled to the pages actually processed when it finishes. Failed or cancelled jobs release their reservation.
  • When included credits are exhausted, jobs continue as overage. Set a spending cap on the billing page to stop at a budget: past it, new jobs get 402 spending_cap_reached.
  • GET /api/v1/usage shows included / used / remaining, overage pages and amount, pages in flight and the recent ledger.

Sandbox vs live

Sandbox (dxk_test_)Live (dxk_live_)
planAny plan, including FreeAPI Starter
billingNever. credits.billed is always false; usage stays at zero.Reserve → settle per page; overage metered to Stripe
processingCheap operations (merge, split, rotate, compress, protect, …) run for real on the same workers. OCR is queued to the real worker when the API runs with the mock OCR engine; otherwise a canned fixture result (provider: sandbox-fixture) is returned instantly.Everything runs for real
limits100 pages per job · 30 rpm · 2 concurrent jobsPlan page cap · 60 rpm · 2 concurrent jobs
dataDeleted after 24h. Responses carry sandbox: true, downloads X-Docaxo-Sandbox: true.Inputs and results expire after 24h
webhooksDelivered with livemode: false; http URLs allowed in developmentlivemode: true; https only

Endpoint reference

MethodPathSummaryScope
POST/api/v1/filesUpload (multipart) or request a presigned upload URLdocuments:write
GET/api/v1/filesList filesdocuments:read
GET/api/v1/files/{file_id}Retrieve a filedocuments:read
POST/api/v1/files/{file_id}/completeFinish a presigned uploaddocuments:write
DELETE/api/v1/files/{file_id}Delete a filedocuments:write
POST/api/v1/jobsCreate a job (Idempotency-Key supported)jobs:write
GET/api/v1/jobsList jobs (filter by status)jobs:read
GET/api/v1/jobs/{job_id}Retrieve a jobjobs:read
POST/api/v1/jobs/{job_id}/cancelCancel a queued or running jobjobs:write
GET/api/v1/jobs/{job_id}/resultDownload the result (format=download | json | url)jobs:read
POST/api/v1/webhooksCreate a webhook endpoint (secret shown once)webhooks:manage
GET/api/v1/webhooksList webhook endpointswebhooks:manage
GET/api/v1/webhooks/{webhook_id}Retrieve a webhook endpointwebhooks:manage
PATCH/api/v1/webhooks/{webhook_id}Update url / events / enabledwebhooks:manage
DELETE/api/v1/webhooks/{webhook_id}Delete a webhook endpointwebhooks:manage
POST/api/v1/webhooks/{webhook_id}/testSend a signed webhook.test event nowwebhooks:manage
GET/api/v1/webhooks/{webhook_id}/deliveriesDelivery logwebhooks:manage
GET/api/v1/accountAccount, plan, key and limitsany
GET/api/v1/usageCredits, overage and ledger for this periodusage:read
GET/api/v1/operationsOperation catalogueany
GET/api/v1/errorsError code referencenone
GET/api/v1/pricingPricing and limitsnone
GET/api/v1/openapi.jsonThis API as OpenAPI 3.1none

Static list — the live spec is served at https://docaxo.com/api/v1/openapi.json and Swagger UI at https://docaxo.com/api/v1/docs.

DOCAXO API — documentation