We are in official beta.

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. www.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.

A bad key answers 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.
Two probes that settle it
# 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:

Error response body
{
  "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.

A failed call is not billed. Tokens are reserved before the operation runs and refunded in full if it throws or returns anything outside 2xx. If you see a charge for a failed call, that is a bug worth reporting — send us the correlation id.

Status codes you will actually see

StatusTypical codeWhat went wrongWhat 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_… or Authorization: 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:

Key verification probe
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:

402 on a per-page operation with an undeterminable page count
{
  "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:

  1. 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.
  2. Quote the job first. POST /api/quote is free and returns holdTokens — the number the 402 actually fires on — alongside affordable and shortfall. 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 password field and it is decrypted for you.
  • Or strip the protection once with /api/UnlockPdf and 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.

  • 400 from the Word and DOCX-conversion endpoints: WordExtractText, ConvertDocxToHtml, ConvertDocxToMarkdown and ConvertDocxToPdf. These used to answer 400 INVALID_INPUT for a file that was not a Word document, and a 500 for one with damaged content inside. If your integration branches on INVALID_INPUT, add UNREADABLE_DOCUMENT.
  • 400 from the Word-template endpoints: WordPopulateTemplate and ComposeFill with a Word (docx) template, with reason archive or package. These also used to answer 400 INVALID_INPUT. A template that is a real Word document but cannot be filled has its own codes: see below.
  • 422 from the personal vault: VaultIngestSelfDocument and VaultReExtractDocument (a re-extract also returns the documentId).
400 from WordExtractText
{
  "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"
}
reasonWhat went wrongWhat 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 .docx is 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 the documentId), except AiJobRun, which answers its own 413 AI_JOB_TOO_LARGE for instructions.docx with the same fields. The body carries limit and stage, plus maxBytes or maxEntries. 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 carries limit (its name) and max (its value). Open the file in Word and save it as a new .docx. Answered by WordExtractText, the ConvertDocxTo… endpoints, the personal vault, and AiJobRun for its instructions.docx.
  • 413 RENDER_OUTPUT_TOO_LARGE: ComposeFill (an HTML template, inline or saved) and ImportHtmlTemplate. 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 carries maxChars. 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: ComposeFill with outputFormat: "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. reason says which (pages, time or memory), and the body carries all three limits: maxPages, maxSeconds and maxMemoryMB. 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.

codereasonWhat 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.

422 from WordPopulateTemplate
{
  "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, TelnyxSendSms and AiJobRun (with your own AI connection) answer 400 with reason control or aboveLatin1, not billed. The REST and HTTP request pipeline steps fail with the same code (a step refusal carries no reason). No retry can fix this one, so do not retry it.
  • ConnectionTest answers 200 with ok: false, the code and reason beside the detail sentence.

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