Developer API

A clean API for clean Markdown.

One POST turns a PDF or image into LLM-ready Markdown and a structured JSON payload. Sync for small docs, async with signed webhooks for big ones — and zero document-retention on every call, EU-hosted.

See API plans

API access is included on every paid plan (from €5.89/mo). New accounts get 100 free pages to try HexRead on the web first.

Sync & async

Small docs return Markdown inline (200). Large ones return a job (202) you poll, stream over SSE, or receive by webhook.

Built to script

Idempotency keys, stable error codes with a request_id, RateLimit-* headers, and an OpenAPI 3.0.3 contract you can codegen from.

Zero document retention

Your document streams through memory and is deleted right after conversion — no document backups, never training data.

Base URL & authentication

The API is a plain HTTPS + multipart service. Authenticate every request with a bearer API key.

Base URL https://api.hexread.com/v1
Auth header Authorization: Bearer hr_live_…

Mint a key from your account (or POST /v1/keys) — the full secret is shown exactly once, so store it like a password. Keys carry least-privilege scopes (convert, jobs:read, usage:read). API keys require a paid plan; a missing or invalid key returns 401.

Quickstart

Sign in, mint a key, then pick your interface:

curl
# Small docs return Markdown inline (200); large ones return a job (202).
curl -fsS https://api.hexread.com/v1/convert \
  -H "Authorization: Bearer $HEXREAD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F file=@report.pdf \
  -F model=auto \
  -F formats=md,json | jq -r .markdown
TypeScript SDK · npm i @hexread/ts-client
import { convertFile, withRetry } from '@hexread/ts-client';

const base = 'https://api.hexread.com/v1';
// backoff retry with a STABLE Idempotency-Key threaded per attempt, so a retry never double-bills;
// returns a sync result or an async job handle
const out = await withRetry((ctx) =>
  convertFile(file, { model: 'auto', idempotencyKey: ctx.idempotencyKey }, base),
);
console.log(out.kind === 'sync' ? out.result.markdown : `job ${out.job.job_id}`);
CLI
curl -fsSL https://hexread.com/install.sh | sh   # cosign-verified
hexread login                                     # device grant → OS keychain
hexread convert report.pdf -o report.md           # sync/async auto; live progress
hexread batch "invoices/*.pdf" -o out/ --concurrency 4

Convert options

  • file The PDF or image to convert (required).
  • model auto (default — routes by content), mineru-2.5, granite-docling, or paddleocr-vl.
  • formats Comma-separated: md, json. Default md,json.
  • prefer sync or async — force the path regardless of document size.
  • webhook · webhook_secret Set delivery=webhook to receive a signed callback when the job finishes.
  • Idempotency-Key Header (≤255 chars) — safe retries; a replay returns Idempotency-Replayed: true.

Convert returns clean Markdown plus a JSON payload of per-page Markdown and conversion metadata (model, page count, checksum, timing) — not a raw layout tree.

Endpoints

Convert

  • POST /v1/convert Upload a PDF or image; get Markdown + JSON inline (small docs) or a job handle (large ones).

Jobs

  • GET /v1/jobs/{id} Async job status — metadata only, never document content.
  • GET /v1/jobs/{id}/events Server-sent progress stream, page by page.
  • GET /v1/jobs/{id}/result Read-once result (also /markdown, /json) — purged the moment you fetch it.
  • DELETE /v1/jobs/{id} Cancel a queued or in-flight job.

Keys

  • GET /v1/keys List your API keys (masked — the secret is never returned again).
  • POST /v1/keys Mint a key — the full secret is shown exactly once.
  • POST /v1/keys/{id}/rotate Rotate — old key revoked, new secret shown once.
  • DELETE /v1/keys/{id} Revoke a key, effective immediately.

Usage & meta

  • GET /v1/usage Current-period pages + concurrency meters. No document data.
  • GET /v1/version Service version and the selectable model registry.
  • GET /v1/openapi.json This API as a machine-readable OpenAPI 3.0.3 document.

The complete, machine-readable contract — every parameter, response, and error code — is served at GET /v1/openapi.json (OpenAPI 3.0.3). Point your generator at it to build a client in any language.

Errors

Every non-2xx response is the same JSON envelope. Branch on the stable code (never the message), and log the request_id.

Error envelope
{
  "type": "validation_error",
  "code": "file_too_large",
  "message": "The uploaded file exceeds the size limit for your plan.",
  "request_id": "req_98fca6c87faace9b"
}

Each code maps to a fixed HTTP status and type — e.g. quota_exceeded (402), rate_limited (429), file_too_large (413), unsupported_media_type (415). The request_id is also returned as the X-Request-Id header for support.

Rate limits, quota & headers

Starter 60 req/min
  • Burst 120
  • 500 pages / mo
  • 2 concurrent conversions
  • Up to 100 MB · 500 pages / doc
Pro 120 req/min
  • Burst 240
  • 3,000 pages / mo
  • 3 concurrent conversions
  • Up to 100 MB · 1,000 pages / doc
Power 240 req/min
  • Burst 480
  • 10,000 pages / mo
  • 5 concurrent conversions
  • Up to 200 MB · 1,500 pages / doc
  • RateLimit-Limit / -Remaining / -Reset Your window: ceiling, calls left, seconds to reset (IETF draft).
  • RateLimit-Policy The active policy, e.g. 120;w=120 (120 requests per 120 s).
  • X-HexRead-Quota-Pages-Remaining Whole pages left in your monthly allowance.
  • Retry-After Seconds to wait before retrying, sent on every 429.
  • X-Request-Id Echoes the envelope request_id — quote it in support requests.

On 429, honour Retry-After and back off exponentially (the SDK's withRetry does this for you). Need higher limits or a pooled team allowance? Compare plans or talk to us about Custom.

Webhooks

For async jobs, set delivery=webhook with a webhook URL and a webhook_secret on POST /v1/convert. When the job finishes, HexRead POSTs a signed job.completed (or job.failed) event; fetch the read-once result with its one-time token.

Verify the signature (raw body)
import { verifyWebhookSignature } from '@hexread/ts-client';

// In your webhook route — verify against the RAW request bytes before trusting it:
const ok = await verifyWebhookSignature(
  process.env.HEXREAD_WEBHOOK_SECRET, // your per-request webhook_secret
  req.headers['x-hexread-signature'], // "t=<unix>,v1=<hmac-sha256>"
  rawBody,                            // exact bytes received (not re-serialised JSON)
);
if (!ok) return res.status(400).end();          // forged, or timestamp skew > 5 min
const event = JSON.parse(rawBody);              // { type: "job.completed", job_id, result_url }

Signature header X-HexRead-Signature: t=<unix>,v1=<hmac> is HMAC-SHA256 over <t>.<raw-body> keyed by your secret. Verify the raw bytes and reject a timestamp skew over 5 minutes to stop replays.

Versioning & SDKs

Stable /v1

Versioned in the path. Changes are additive-only; anything breaking ships as /v2. Pin to /v1 — there's no dated version header to track.

TypeScript SDK

@hexread/ts-client — typed convert, auto-retry, and verifyWebhookSignature. Generated from the same spec, so it never drifts.

CLI & any language

A cosign-verified Go hexread CLI (install.sh), or generate a client for your stack from /v1/openapi.json.

Build with HexRead.

Start free on the web in seconds, then add a paid plan to drop in the API when you're ready to automate.

Compare plans