Core concepts
Errors
Every error response from the API has the same JSON shape. Branch on code, show message, and quote request_id when you contact support.
Error envelope#
{
"code": "not_found",
"message": "Document not found",
"detail": "Document not found",
"request_id": "9f2c1a7e4b0d3e11"
}| Field | Description |
|---|---|
code | Stable, machine-readable snake_case string. Always present. Use it in your code. |
message | Human-readable explanation in English. Always present. Wording may change; do not parse it. |
detail | Legacy field kept for backward compatibility: always the same string as message, except for request validation errors (422 validation_error) it is the list of field errors. |
request_id | Same value as the X-Request-Id response header. Include it in support requests. |
extra | Optional object with error-specific data, for example quota numbers (current, limit), the missing plan feature, or retry_after. |
Clients should branch on code, not on message. New codes may be added at any time, so always fall back to handling by HTTP status for codes you do not recognize.
Generic codes by status#
When no more specific code applies, code is derived from the HTTP status:
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request, for example a file that is not a PDF. |
| 401 | unauthenticated | Missing, invalid, revoked or expired API key. |
| 402 | payment_required, quota_exceeded | A plan feature or limit blocks the request. Specific codes such as feature_not_in_plan or documents_per_month are usual. |
| 403 | forbidden, insufficient_scope | Authenticated, but not allowed: wrong role, endpoint not available to API keys, or a missing scope. |
| 404 | not_found | The resource does not exist or belongs to another organization. Tenancy never leaks existence. |
| 405 | method_not_allowed | The path exists but not with this method. |
| 409 | conflict | The resource is in a state that does not allow this action, for example document_not_draft. |
| 410 | gone | The resource existed but is no longer available. |
| 413 | payload_too_large | Request body over the limit (30 MB for most endpoints). |
| 415 | unsupported_media_type | Wrong content type for an upload. |
| 422 | validation_error | The body or parameters failed validation, or a business rule rejected them. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 500 | internal_error | Unexpected server error. Safe to retry idempotent requests. |
| 502 | upstream_error | A dependency (email, SMS, registry lookup) failed. |
| 503 | service_unavailable | Temporarily unavailable. Retry with backoff. |
See the error code reference for the specific codes you are most likely to meet.
Validation errors#
When the request does not match the schema, the status is 422, code is validation_error, and detail lists every problem with its location:
{
"code": "validation_error",
"message": "Request validation failed",
"detail": [
{"loc": ["body", "fields", 0, "position_x"], "msg": "Input should be less than or equal to 100", "type": "less_than_equal"}
],
"request_id": "0b7e51f2c9a84d60"
}Business-rule failures also use 422 but carry a specific code and a string detail, for example signer_missing_signature_field when sending a draft where a signer has no signature field.
Handling errors#
# -w prints the status; the body is the error envelope.
curl -sS -w '\n%{http_code}\n' "https://api.sign.yt/v1/documents/00000000-0000-0000-0000-000000000000" \
-H "Authorization: Bearer $SIGNYT_API_KEY"import os
from signyt import Signyt, NotFoundError, RateLimitError, SignytError
client = Signyt(os.environ["SIGNYT_API_KEY"])
try:
client.documents.get("00000000-0000-0000-0000-000000000000")
except NotFoundError as e:
print("missing:", e.code, e.request_id)
except RateLimitError as e:
print("slow down, retry after", e.retry_after)
except SignytError as e:
print(e.status, e.code, e.message, e.extra, e.request_id)import { Signyt, NotFoundError, RateLimitError, SignytError } from "@signyt/sdk";
const client = new Signyt(process.env.SIGNYT_API_KEY!);
try {
await client.documents.get("00000000-0000-0000-0000-000000000000");
} catch (e) {
if (e instanceof NotFoundError) console.log("missing:", e.code, e.requestId);
else if (e instanceof RateLimitError) console.log("retry after", e.retryAfter);
else if (e instanceof SignytError) console.log(e.status, e.code, e.message, e.extra, e.requestId);
else throw e;
}What to retry#
- Retry
429,500,502,503and network errors with exponential backoff and jitter. ForPOSTrequests send an Idempotency-Key so retries cannot create duplicates. - Do not retry other
4xxresponses unchanged: fix the request, the state, or the plan first. - The SDKs retry
GET,PUTandDELETEon429/5xx/network errors (2 retries by default, honoringRetry-After) and never retryPOSTorPATCHautomatically.