Folio

Errors

Every failure returns the same shape, with a stable code to switch on and a message written for a person to read.

{
  "error": {
    "code": "no_text_layer",
    "message": "This PDF is images, not text. Reading it requires OCR.",
    "needsOcr": true
  }
}

Rows marked carry needsOcr: true.

StatusCodeMeaning
400empty_bodyThe request carried no document at all.
400missing_keyA JSON body arrived without the object key naming what to convert.
401unauthorizedAbsent, malformed, unknown, or revoked key. All four answer the same, so probing learns nothing.
402ocr_quota_exceededRefused before any OCR ran, so nothing was billed. The page count is known in advance.
402quota_exceededThe document allowance for this period is spent.
404not_foundThe key does not exist, or belongs to another organization. Both answer the same.
413resource_limitUsually a decompression bomb rather than a real document.
413too_largeOver the 100MB ceiling.
415unknown_formatNeither the content signature nor the extension named a supported format.
422encryptedRemove the password and send it again.
422incompleteRe-export it from the application that produced it.
422malformedStructurally unusable, often truncated.
422no_text_layerEvery page is a scan. Carries `needsOcr: true`, this is the paid path, not a mistake you made.
422unsupportedA format anydoc recognises but cannot read.
500conversion_failedNothing more specific was known. Worth reporting.
503uploads_unavailableThe presigned upload path is down. Smaller documents still convert.

The one worth handling specially

no_text_layer is not a mistake you made. It means every page of the PDF is a scan, so there is nothing to extract and the document needs OCR, which is the paid path rather than a fault. It is the only error that carries needsOcr, so it can be branched on directly.

if (!response.ok) {
  const { error } = await response.json();

  if (error.needsOcr) {
    // Every page is a scan. Retry on a plan with OCR pages available.
  }
  if (error.code === 'ocr_quota_exceeded') {
    // Refused before any OCR ran, so nothing was billed for this attempt.
  }
  throw new Error(error.message);
}

Nothing failed is ever billed

Usage is recorded after a conversion succeeds, never before. A refusal costs nothing, whether it was for quota, for size, or for an unreadable file. And ocr_quota_exceeded is decided from the page count, before any OCR model is called at all.

Authentication answers uniformly

An absent key, a malformed one, one that never existed and one that was revoked all return the same 401 with the same message. Distinguishing them would only help someone working out which keys are real.