09 · ERRORS

Errors, rate limits and quotas

The error envelope, every status code, the codes worth branching on, rate limits, and the hard limits to design around.

All documentation

Every failure comes back in one shape, and most of them mean something specific enough to act on.

The error envelope

json
{
  "error": {
    "code": "validation_error",
    "message": "One or more fields are invalid.",
    "details": [
      { "field": "recipients.0.email", "message": "Invalid email" }
    ]
  }
}

code is the contract and is safe to branch on. message is prose written for a human and may be reworded, so never match on it. details appears on validation errors and names each bad field - log it, or you are debugging blind.

Some errors carry extra keys alongside those three: requiredPlan on a 402, known_roles and missing_roles on a template send, file_ids on an unknown file, cap on a limit.

Four codes carry a suffix - prefix-match them
signer_missing_signature_field:<ids>, max_recipients_exceeded:10, duplicate_email:<email> and entitlement_required:<key> append their detail after a colon. An equality check against the bare name will never match. Use code.startsWith("...") for these four.

Status codes

What each status means here
FieldTypeNotes
400Malformed, or a domain rule you can fix. Do not retry unchanged.
401Key missing, malformed, revoked, expired, or its creator lost access. Do not retry.
402The plan does not include the API, or a plan limit was reached. Not a card problem.
403Missing scope, or the workspace IP allowlist refused you.
404No such object - or it exists in the other mode.
409Valid request, wrong state: already sent, already signed, embedding not configured.
413 / 415File too large, or a type we do not accept.
429Rate limited. Back off and retry.
5xxOur problem. Retry with backoff.

Only 429 and 5xx are worth retrying automatically. Retrying a 400 or a 402 just produces the same failure more often.

Codes worth knowing by name

Common codes
FieldTypeNotes
api_not_enabled402Workspace plan has no API access. Carries requiredPlan.
insufficient_scope403The key lacks the scope named in the message.
document_not_found404Missing, or created in the other mode.
document_not_draft409You tried to send something already out.
document_not_in_progress409Cancel and embed links both need a live document.
unknown_role400Template role mismatch. Carries known_roles.
missing_roles400A template role was left unfilled. Carries missing_roles.
field_out_of_bounds400A field does not fit on its page.
embedding_not_configured409No origins on the workspace embed allowlist.
password_protected_pdf400Remove the password before uploading.
corrupt_pdf400The file could not be parsed.
unsupported_file_type415Detected from the bytes, not the declared type.

Rate limits

10 requests per minute per key. The limit is per key, so splitting work across several keys to go faster is circumventing it - ask for a higher limit if you have a genuine bulk need.

http
x-ratelimit-limit: 10
x-ratelimit-remaining: 8
x-ratelimit-reset: 42

x-ratelimit-reset is the seconds left in the current window and counts down toward zero. A 429 additionally carries a standard retry-after header, which is the simplest thing to honour.

Branch on the 429 status, not on a code
The body of a rate-limited response reads { "error": { "code": "request_failed" ... } }. There is no rate_limited code, so a switch case on one will never fire. Check res.status === 429.
javascript
async function withRetry(call, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    const res = await call();
    if (res.status !== 429 && res.status < 500) return res;
    if (i === attempts - 1) return res;

    const retryAfter = Number(res.headers.get("retry-after"));
    const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : Math.min(2 ** i * 1000, 30_000);

    // Jitter, or every worker retries in the same instant.
    await new Promise((r) => setTimeout(r, waitMs + Math.random() * 250));
  }
}

Hard limits

Design around these
FieldTypeNotes
Requests10 / minPer key.
File size25 MBPer upload.
Files per document10Merged in order.
Recipients per document10Signers and CCs combined, on POST /documents.
Recipients per template send20Genuinely higher than the direct path.
Fields per document500
Embed link lifetime60 - 3600 sDefaults to 900.
Download URL lifetime600 sMint a fresh one rather than storing it.
API keys10Per workspace.
Webhook endpoints5 per modeFive test and five live.
Webhook signature tolerance300 sOlder deliveries should be rejected.
List page size1 - 100Defaults to 20.

What API sends cost

Business includes 40 API documents a month and Ultimate includes 200, flat per workspace rather than per seat. That is a free threshold, not a ceiling: document 41 still sends, billed at $0.40. Read usage on GET /account to see where you are in the current period.

A live API send counts against your normal monthly document allowance as well, because it is the same pool the web app draws from. Sandbox sends count against neither.

Mistakes worth avoiding

  • Retrying a 4xx. Only 429 is retryable in that range.
  • Branching on message. It is prose. code is the contract.
  • Swallowing details on a 400. It names the exact field that is wrong.
  • Retrying a send after a timeout without checking. The document may already exist. Look it up first, or you will send twice.