Errors
One shape, always.
{
"error": {
"code": "invalid",
"message": "no value for guest_name",
"hint": "A contract that ships with a placeholder still in it is worse than one that failed to send."
}
}
The message is frequently the database's own sentence, passed
through rather than replaced. It is more specific than anything this layer
could write, and the layer that knows why something was refused is the layer
that refused it.
| Status | Code | Means |
|---|---|---|
| 400 | bad_request | The request could not be read at all. |
| 401 | unauthorized | No key, or a key that is not live. Unknown, revoked and expired all answer the same way — a caller who could tell them apart could enumerate. |
| 403 | forbidden | The key is live and its scopes do not carry this. |
| 403 | terms_not_accepted | The business was asked to agree to SignSealer's updated terms and thirty days have passed without it, so nothing new can be sent until an owner or admin agrees to them in the dashboard. Documents already sent can still be signed and everything can still be read. Retrying will not help until then. |
| 403 | assertion_rejected | Your application's approval (POST / |
| 404 | not_found | No such thing, in this account. A thing in another account answers the same way, and deliberately: confirming it exists would be the leak. |
| 405 | method_not_allowed | That path exists under another method. Said separately from 404 because it saves an hour. |
| 402 | quota_exceeded | The account's credit is used up and could not be reloaded, or it reached the spending limit it set itself. Nothing is wrong with the request. |
| 409 | conflict | An idempotency key reused for a different body, or the first attempt still running; or the thing named has moved on, such as a template revised from a version that is no longer the newest. The message says which, and the hint names the current one. |
| 409 | template_changed | A start named the template version it showed (expected_template_version), and a newer one has been published since. Nothing was made: show the person the current wording. |
| 409 | paragraphs_changed | A signature named the conditional paragraphs its page drew (paragraphs), and the first signer's answers now call for different ones. Nothing was signed: fetch the link again and show the paragraphs it has now. |
| 409 | document_changed | The text is not the text the person approved (document_sha256). Nothing was signed: show them the document as it is now and ask them to approve it again. |
| 409 | assertion_replayed | Another approval already used this approval id (jti). Every approval has its own; the same approval sent again unchanged answers with the signing it made. |
| 413 | too_large | Larger than a signing request ever is. |
| 422 | invalid | The body did not validate, or the engine refused it. The message is the engine's own sentence. |
| 429 | rate_limited | Too many attempts, or a business past the signing links its standing allows in an hour or a day (Limits and paging). The message names the budget, the hint says when, and the Retry-After header says how many seconds. |
| 429 | busy | A photo on a signing link was not read because others are being read just now. Nothing was kept: send the same photo again after the Retry-After header's seconds. |
| 429 | allowance_reached | A tablet's start made with no network, synced when the business cannot take another signing (its free signings for the month used, a balance that does not cover the next, its own spending limit, or a stopped subscription). Nothing was made: keep it and send it again; the Retry-After header says when. Any other start is answered 402 quota_exceeded. |
| 500 | internal | Ours. Quote the x-request-id. |
Retrying
Retry 429 and 500, with backoff. Do not retry
422 — the request will be refused the same way every time, and the
message says why. Retrying anything that creates something means sending an
idempotency key; our own SDK mints one for you.
402 is not 429
A 402 means the account needs money or the membership, not a
pause: Free's 10 signings are used, the prepaid credit has run out, or the
business's own spending limit is reached. It is the one refusal that a retry
cannot fix: credit has to be added (automatic reload usually does it first),
the membership started, or the month has to turn. Treat it as you would a
declined card — surface it to the person who can act on it, and stop. The
response carries the same shape as any other error, and the hint
names the way out.
One start is answered otherwise: a counter tablet's, made with no network
and sent when it synced. The person has already signed at the counter, so the
answer is 429 allowance_reached, for the tablet to keep it and
send it again. Nothing is made until the business can take another signing,
and then it counts like any other.
Every successful send carries x-agreements-remaining when the
account has a fixed number of signings left to count down: how many more
documents it can send before it needs credit. A document out for signature
holds its place, so the number goes down with the send that sent it, and comes
back if that document is voided, declined or expires unsigned; one that is never
signed costs nothing. It is on the send's answer because that is the call an
integration already makes. It is absent when there is nothing to count down. Watch it and you will never meet a
402 unprepared — or subscribe to billing.quota_approaching
and do not watch at all.
A 429 carries Retry-After, in seconds. Wait that
long rather than backing off by a guess: the rate-limit window is a minute, so
three quick retries all land inside the same window, fail identically, and
spend the next window's budget as well. The Node client honors the header —
it sleeps through a short wait and hands a long one back to you as
error.retryAfter rather than blocking your call for a minute.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 10 agreements a month.
Get an API key