Troubleshooting
What the API is telling you when it says no — every error carries a machine-readable code and a reason, so most problems are one field away from an answer.
I am getting 404 on every request
You are calling the wrong host. The API lives on
https://api.docbutterfly.com and every endpoint hangs off /api/… there.
docbutterfly.com is this website — marketing pages, the portal, these docs
— and it has no /api routes at all, so it answers 404 to every one of
them whether your key is perfect, wrong, or absent.
401, never 404. So a 404 on a
request you are sure about is a host problem, not a credential problem — and a 401
is actually good news, because it proves you reached the API.
# 1. Is the API there? No key needed; JSON back means the host is right.
curl -i "https://api.docbutterfly.com/api/health"
# 2. Is my key the problem? A deliberately WRONG key must answer 401.
curl -i -X POST "https://api.docbutterfly.com/api/ConvertHtmlToPdf" \
-H "X-API-Key: not-a-real-key" -H "Content-Type: application/json" \
-d '{"html": "<p>x</p>", "returnBase64": true}'
# 401 -> host correct, fix the key. 404 -> host wrong, fix the URL.
The full picture, including how a webhook URL is built, is on
Base URL & hosts. If the host is right and you still get
404, check the endpoint spelling against the
API Reference — routing ignores case, so
/api/converthtmltopdf is fine, but a name that is not in the catalog is not.
Read the error envelope first
Every error response — from every endpoint — has the same shape:
{
"success": false,
"error": "Payment Required", // short human label
"message": "Insufficient tokens", // human-readable detail
"code": "INSUFFICIENT_TOKENS", // stable machine code โ branch on this
"required": 50, "available": 12 // extra fields, per error
}
Branch your integration on code, not on message. Codes are stable
UPPER_SNAKE strings; messages get reworded. The extra fields carry the part you can act
on — required and available on a 402, pageCount and
maxPagesPerRequest on a 413, and so on.
Status codes you will actually see
| Status | Typical code | What went wrong | What to do |
|---|---|---|---|
400 |
INVALID_JSON |
The request body is not valid JSON. The message quotes the parse error. | Fix the body. Nothing was reserved, so nothing was billed. |
400 |
INVALID_INPUT, BAD_REQUEST |
The JSON parsed but a parameter is missing, malformed, or out of range. | Check the endpoint's parameters in the API Reference. |
401 |
UNAUTHORIZED |
No API key, or a key that resolves to no account. | See below. |
402 |
INSUFFICIENT_TOKENS |
The balance cannot cover the reservation for this call. | See below. |
403 |
FORBIDDEN |
The key is valid but the account is deactivated. | Contact us — a key change will not fix this. |
404 |
NOT_FOUND |
Wrong host, wrong path, or a webhook slug that is no longer published. | If every call 404s, see above — it is the host. Otherwise check the endpoint spelling; re-publish the workflow in the Orchestrator. |
413 |
PAYLOAD_TOO_LARGE, DOC_AI_TIER_PAGE_LIMIT |
Document AI operations process at most 30 pages per request. The body returns pageCount and maxPagesPerRequest. |
Split the document first (/api/SplitPdf or /api/ExtractPdfPages) and submit page ranges. |
422 |
ENCRYPTED_PDF |
The PDF is password-protected or encrypted, so it cannot be re-serialized as-is. | See below. |
429 |
RATE_LIMITED |
An upstream AI provider is throttling — not a limit we impose. | Retry with backoff. There are no hard rate limits on the API itself. |
500 |
INTERNAL_ERROR |
An unexpected fault. The reservation is refunded automatically. | Retry once; if it persists, send us the X-Correlation-Id. |
500 |
BILLING_ERROR |
Tokens could not be reserved (storage contention), so the operation never ran. | Retry. Nothing was charged. |
502 |
AI_UNREACHABLE, AI_REQUEST_FAILED |
An AI-backed endpoint could not reach or was rejected by its provider. | Retry. Non-AI endpoints are unaffected. |
503 |
SERVICE_UNAVAILABLE |
The feature is built but not configured in this environment — email, e-signature and AI endpoints each need their own credentials. | The message names what is missing. Tell us which endpoint you need turned on. |
401 — the key is missing or does not resolve
Two different messages, two different causes:
- “API key required” — no key arrived at all. Send it as
X-API-Key: df_…orAuthorization: Bearer df_…. A key in the query string is not accepted; that transport was removed on purpose, because full request URLs are written to diagnostic logs in plaintext. - “Invalid API key” — a well-formed key that matches no account. Usually a copy/paste artifact (trailing whitespace, a truncated value, a smart quote from a document) or a key that was replaced by a later regenerate.
Verify the exact key you are sending, in one free call that never touches your balance:
curl "https://api.docbutterfly.com/api/account" -H "X-API-Key: $DOCBUTTERFLY_API_KEY"
# 200 { "authenticated": true, "clientId": "...", "isActive": true,
# "tokenBalance": 940, "unlimited": false, "tier": "free" }
# 401 { "authenticated": false, "error": "UNAUTHORIZED", "message": "Invalid API key" }
If the probe says authenticated: false, the key is the problem — not the endpoint you
were calling. Regenerate from Portal → API Key, remembering that
the old key stops working the instant the new one is issued.
402 — insufficient tokens
The body tells you the arithmetic: required is what the platform tried to reserve,
available is what you had, and operation is what you were calling.
The part that surprises people is which number fires the 402. Tokens are reserved before the operation runs, and for per-page operations the reservation is exact only when we can count the pages up front. If the page count cannot be determined from the request, the platform reserves a refundable worst case — the per-page rate multiplied by the 30-page cap — and the response says so:
{
"success": false, "code": "INSUFFICIENT_TOKENS",
"required": 150, "available": 40, "operation": "pdf_analyze_ai",
"perPageRate": 5, "maxPages": 30,
"note": "\"required\" is a refundable worst-case hold (5 tokens/page x up to 30 pages),
reserved because the page count could not be determined up front;
the unused portion is refunded after processing."
}
Two ways out, in order of preference:
- Send the document in a field we can count. The reservation then matches the real document instead of the worst case, and a one-page extraction reserves one page's worth.
- Quote the job first.
POST /api/quoteis free and returnsholdTokens— the number the 402 actually fires on — alongsideaffordableandshortfall. See Usage & Billing.
If the balance is genuinely spent, the monthly allowance refills on the first of the month, and purchased tokens can be added from Portal → Top Up.
422 — encrypted or password-protected PDFs
Bank statements, payroll runs and financial reports routinely ship with an owner password. They open
without prompting, but they carry permission restrictions, and writing a new PDF from one produces a
corrupt file. Rather than hand you that, those requests are refused with a 422 and the
code ENCRYPTED_PDF.
Two fixes, both supported:
- Pass the document password in the optional
passwordfield and it is decrypted for you. - Or strip the protection once with
/api/UnlockPdfand work from the result.
Slow first call, then fast
Browser-backed endpoints — ConvertHtmlToPdf, CaptureWebPage and
anything that renders a page — load a headless Chromium on a cold start. The first call after a
quiet period can take 10–30 seconds; subsequent calls are seconds. This is expected, not a fault.
- Set your HTTP client timeout to at least 120 seconds for those operations and for any pipeline that contains one. Power Automate and Logic Apps both default lower than you want here.
- Retry on network errors and 5xx only. Do not blanket-retry on timeout — a retry of a slow-but-successful call runs the work again and bills it again.
- A pipeline is billed once, up front, for the whole run — not per step.
Getting help — what to send us
Every response carries an X-Correlation-Id header, and that same id is on the log lines and
the usage row for the request. It is the single most useful thing you can give us: with it we can find
exactly what happened, including what the operation was charged and why.
You can also set the id yourself — send your own X-Correlation-Id request header (a
Power Automate run id, for example) and we will use it, so a trace joins up across both systems.
Along with the correlation id, tell us the endpoint, the code from the response, and
roughly when it happened. Your own call history is in
Portal → Usage, and programmatically at
GET /api/account/ledger.
Still stuck?
Reproduce it against your own input in the testbed, or send us the correlation id and we will look it up.
API Testbed Contact us