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. |
400 |
UNREADABLE_DOCUMENT |
A Word, DOCX-conversion or Word-template endpoint could not read the .docx you sent. The body's reason says why. These endpoints used to answer INVALID_INPUT here. |
See below. |
400 |
CONNECTION_CREDENTIAL_UNSENDABLE |
The credential stored on the connection holds a character no HTTP header can carry, so nothing was sent. | Re-enter the credential on the Connections page. See below. |
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. |
413 |
DOCX_EXPANDED_TOO_LARGE, RENDER_OUTPUT_TOO_LARGE, PDF_RENDER_TOO_LARGE |
A Word document unpacks past its size limit, a filled template would grow past its size or work limit, or a filled HTML template would print to more pages, or take more time or memory, than one PDF may. The body names the limit. | See below. |
422 |
ENCRYPTED_PDF |
The PDF is password-protected or encrypted, so it cannot be re-serialized as-is. | See below. |
422 |
UNREADABLE_DOCUMENT, DOCX_MARKUP_TOO_COMPLEX |
The personal vault could not read a document you added, or a Word document's internal structure is too complex to read safely. | See below. |
422 |
DOCX_TEMPLATE_INVALID, DOCX_TEMPLATE_DATA_INVALID |
A Word template could not be filled: its markup or tags are broken, or a data value cannot go into a Word document. The body lists each problem. |
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. |
503 |
SERVICE_UNAVAILABLE with reason: "ocr" |
Adding a document to the personal vault: the document-reading service is busy or unavailable right now. The body carries retryable: true. Nothing was stored and nothing was charged. |
Retry in a few minutes. Your file is not the problem. |
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.
UNREADABLE_DOCUMENT: a document that cannot be read
When the document you sent cannot be read, the answer says so with the code
UNREADABLE_DOCUMENT and a reason you can branch on. The call is not billed.
400from the Word and DOCX-conversion endpoints:WordExtractText,ConvertDocxToHtml,ConvertDocxToMarkdownandConvertDocxToPdf. These used to answer400 INVALID_INPUTfor a file that was not a Word document, and a500for one with damaged content inside. If your integration branches onINVALID_INPUT, addUNREADABLE_DOCUMENT.400from the Word-template endpoints:WordPopulateTemplateandComposeFillwith a Word (docx) template, withreasonarchiveorpackage. These also used to answer400 INVALID_INPUT. A template that is a real Word document but cannot be filled has its own codes: see below.422from the personal vault:VaultIngestSelfDocumentandVaultReExtractDocument(a re-extract also returns thedocumentId).
{
"success": false,
"error": "Unreadable document",
"message": "The file could not be read as a Word document: it is not a .docx file, or the file is damaged.",
"code": "UNREADABLE_DOCUMENT",
"reason": "archive"
}
reason | What went wrong | What to do |
|---|---|---|
archive |
The file is not a .docx, or it is damaged: a truncated upload, an older .doc, or a PDF renamed to .docx. |
Check that the base64 covers the whole file. Open it in Word and save it as a .docx. |
package |
The file has the outer shape of a .docx but no document content inside it, or it is another kind of file, such as .xlsx or .odt. |
Save the document again from Word as a .docx. |
markup |
Part of the document's content is damaged. | Open it in Word, which offers to repair it, and save it as a new .docx. |
pdf |
Personal vault only: the file could not be read as a PDF. | Export or scan it to PDF again. |
encrypted |
Personal vault only: the PDF is password-protected. | Remove the password and add it again. |
image |
Personal vault only: the image file is damaged, or its size or dimensions are outside what the reader accepts. | Take the photo again, or save it again as a JPEG or PNG. |
AiJobRun reads an instructions.docx the same way. A damaged one answers
400 AI_JOB_BAD_INSTRUCTIONS with the same reason values
(archive, package, markup), and the message names the file.
A fault on our side while reading your document is never reported as your file's problem: it is a
500 INTERNAL_ERROR, refunded in full. If the personal vault's document-reading service is
busy, the answer is 503 SERVICE_UNAVAILABLE with reason: "ocr" and
retryable: true; nothing was stored or charged, so try again in a few minutes.
413 and 422: a document past a safety limit
Some documents are refused before any work is done on them, because reading or filling them would take more memory or time than any single request may use. Each refusal names the limit it hit, and none of them is billed.
413 DOCX_EXPANDED_TOO_LARGE: a.docxis a compressed archive, and this one unpacks past the size limit. Any endpoint that reads a Word document can answer it (a vault re-extract also returns thedocumentId), exceptAiJobRun, which answers its own413 AI_JOB_TOO_LARGEforinstructions.docxwith the same fields. The body carrieslimitandstage, plusmaxBytesormaxEntries. Split the document, or remove large embedded images and media.422 DOCX_MARKUP_TOO_COMPLEX: part of a Word document is structured in a way that cannot be read safely: for example, one part far larger than Word writes, far too many attributes or namespace declarations on one element, or nesting far deeper than Word produces. Real Word documents do not reach these limits; the file is usually generated by another tool or damaged. The message says which limit in plain words, and the body carrieslimit(its name) andmax(its value). Open the file in Word and save it as a new.docx. Answered byWordExtractText, theConvertDocxTo…endpoints, the personal vault, andAiJobRunfor itsinstructions.docx.413 RENDER_OUTPUT_TOO_LARGE:ComposeFill(an HTML template, inline or saved) andImportHtmlTemplate. The filled document would grow past its size limit, or filling it would take more work than any render may do; the message says which. The body carriesmaxChars. Usually a repeating section fed a very large list, or a value that itself contains template markers. Send fewer rows per call, or split the document.413 PDF_RENDER_TOO_LARGE:ComposeFillwithoutputFormat: "pdf"(an HTML template, inline or saved). The filled document is printed to PDF, and printing it would pass one of three limits: more than 1,000 pages, more than 60 seconds to load, lay out and print (remote images that load slowly count toward it), or more than 1 GB of memory.reasonsays which (pages,timeormemory), and the body carries all three limits:maxPages,maxSecondsandmaxMemoryMB. Very long tables are the usual cause of the memory limit. Nothing is billed, and you get no filled HTML in place of the PDF. Send fewer rows per call, or split the document.
422: a Word template that cannot be filled
WordPopulateTemplate and ComposeFill with a Word (docx) template
answer 422 when the template, or the data you sent for it, cannot be filled. The call is not
billed.
code | reason | What went wrong |
|---|---|---|
DOCX_TEMPLATE_INVALID |
markup |
The template's Word markup is not well-formed, for example a < that never closes, or a text element opened twice. |
DOCX_TEMPLATE_INVALID |
tags |
The template's tags are not balanced: an unclosed or unopened tag or loop, a doubled delimiter, or a loop or raw tag where it cannot expand. |
DOCX_TEMPLATE_DATA_INVALID |
data |
The template is fine but the data is not: a value holds characters a Word document cannot contain, or a raw tag's value is not text. |
The body says where. problems lists up to 10 entries and problemCount
says how many there were in all. Each entry can carry problem (what is wrong, as a stable id),
tag (the tag as you wrote it, cut to 64 characters), closingTag (the tag that
closed a loop it did not open), part (where in the package, such as
word/header1.xml) and, for broken markup, offset (the character position in
that part). Your data values are never echoed back.
{
"success": false,
"error": "Invalid template",
"message": "The template could not be filled: its template tags are not balanced (...). See \"problems\" for each one.",
"code": "DOCX_TEMPLATE_INVALID",
"reason": "tags",
"problems": [ { "problem": "unclosed_tag", "tag": "customer_name", "part": "word/document.xml" } ],
"problemCount": 1
}
A file that is not a Word document at all is 400 UNREADABLE_DOCUMENT
instead. A fault on our side while filling is a refunded 500 INTERNAL_ERROR, never one of these codes.
400 CONNECTION_CREDENTIAL_UNSENDABLE: re-enter the credential
A credential is sent in an HTTP header, and a header cannot carry a control character (such as a line break) or a character above U+00FF (such as a zero-width space or a byte-order mark, which a copy and paste can slip in unseen). Such credentials are refused when a connection is saved, but one saved before that check can still be on a connection. Using it is refused before anything is sent. Nothing of the credential appears in the answer.
RestRequest,TelnyxSendSmsandAiJobRun(with your own AI connection) answer400withreasoncontroloraboveLatin1, not billed. The REST and HTTP request pipeline steps fail with the samecode(a step refusal carries noreason). No retry can fix this one, so do not retry it.ConnectionTestanswers200withok: false, thecodeandreasonbeside thedetailsentence.
The fix: open the connection on the Connections page and type or paste the credential again. For a Key Vault reference, correct the secret in the vault.
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, when it is an id, we will use it, so a trace joins up
across both systems. An id here is letters, digits, . _ : and
-, up to 128 characters. Anything else (a space, a slash, a URL, a longer value) is
ignored and we assign one ourselves, so the X-Correlation-Id on the response is always
the one to quote.
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