Core concepts

Errors

Every failure returns the same JSON shape, and every error you receive links back to this page.

The envelope

Errors never come back as a bare string or a status code alone. They always look like this:

json
{
  "error": {
    "type": "validation_error",
    "code": "missing_recipient_email",
    "message": "email is required when channel is 'email'.",
    "param": "recipients[0].email",
    "doc_url": "https://xosign.ai/docs/api/errors#validation_error",
    "request_id": "req_b07b069f0a83492877e3fa31"
  }
}
FieldAlways presentWhat it is for
typeYesThe error family. Maps one-to-one to the HTTP status, and never changes for a given condition. Branch on this for broad handling.
codeYesThe specific reason. This is the value to branch on when you need to react to one particular failure.
messageYesA human-readable sentence. Safe to log or show to your own staff, but do not parse it — wording can change without notice.
paramOnly when a field is at faultNames the offending input, including its array index — e.g. recipients[2].email.
doc_urlYesLinks to the section on this page for that error type.
request_idYesMatches the X-Request-Id response header. Quote it to support — it is how we find your exact request.

What is stable and what is not

type and code are part of the API contract and will not be renamed within v1. New code values may be added to an existing type, so treat an unrecognised code as its type and handle it generically rather than failing.

Handling errors well

  • Retry 429 after the Retry-After header, and 500/502 with exponential backoff. Do not retry 400, 401, 403, or 404 — the same request will fail the same way.
  • When retrying a write, send the same Idempotency-Key as the original so a request that actually succeeded is not performed twice.
  • Log request_id on every failure. It is the only identifier that lets us find your request in our logs.

Error types

Each heading below is the anchor that the doc_url in your error response points to.

authentication_errorHTTP 401

The request carried no usable API key, so we could not tell who you are.

Send `Authorization: Bearer xo_live_…`. Every failure mode — missing, malformed, revoked, expired, or simply wrong — returns the same message on purpose, so the response cannot be used to probe which keys exist. Check the key was copied whole, has not been revoked in Settings, and that you are calling https://xosign.ai (see the canonical-host warning on the Authentication page).

CodeparamWhereWhat it means
missing_authorizationAny endpointNo `Authorization` header at all. This is the one auth failure with its own code, because it cannot leak anything about which keys exist.
invalid_api_keyAny endpointThe key was malformed, unknown, revoked, expired, or issued for a different mode. All of these return this one code with an identical message — by design, so the endpoint is not an oracle for valid keys.

permission_errorHTTP 403

We know who you are, but this key or this plan is not allowed to do that.

Either the key lacks the scope the endpoint requires (`insufficient_scope`) or the account's plan does not include API access (`api_access_required`). Scopes are fixed at issuance and cannot be widened — issue a new key with the scopes you need.

CodeparamWhereWhat it means
cloud_import_requires_proPOST /importsImporting from Google Drive or Dropbox through the API needs the Pro plan or above. That floor is the API's own: in the app, the Drive and Dropbox buttons work on every plan.
cloud_import_unavailablePOST /importsImporting from Google Drive and Dropbox is switched off for now. DocuSign imports are unaffected.
cloud_import_test_keyPOST /importsImporting from Google Drive and Dropbox needs a live key; test keys cannot bring files in.
import_quota_exceededPOST /importsThis import would pass your plan's limit on files imported from Google Drive and Dropbox in a day, or this month's storage for them is used up. Imports still running count toward the storage limit until their files land, so several started at once can be refused before any file arrives; try again when they finish. Nothing was imported and no file was downloaded; the message says which.
insufficient_scopescopesAny endpoint that requires a scopeThe key is valid but was not issued with the scope this endpoint requires. Keys issued before scopes existed were backfilled read-only, so an old key will hit this on any write or send. Issue a new key with the scopes you need.
api_access_requiredAny endpointThe account's plan does not include API access. Upgrade the plan, then retry with the same key.

validation_errorHTTP 400

The request reached us intact but something in it was not acceptable.

Read `code` for the machine-readable reason and `param` for the exact field — including the array index, e.g. `recipients[2].email`. These are safe to branch on; `message` is human-facing and may be reworded.

CodeparamWhereWhat it means
invalid_sourcesourcePOST /imports`source` was missing or named something we cannot start. It must be `docusign`, `google_drive` or `dropbox`.
invalid_filesfilesPOST /imports`files` must list between 1 and 25 files, and a Dropbox file's `name`, when given, must be text of at most 400 characters. `param` names the entry at fault. Nothing was created.
invalid_file_urlfiles[i].urlPOST /importsA Dropbox entry's `url` is not a link to one Dropbox file. Send a shared link, the Chooser's direct link, or a link from `files/get_temporary_link`. Folder, Transfer and tracking links are refused — for a folder, send the files inside it one by one. Nothing was created.
invalid_file_idfiles[i].idPOST /importsA Google Drive entry's `id` is not a Drive file id. Nothing was created.
invalid_access_tokenaccess_tokenPOST /importsA Google Drive import needs `access_token`: a Google OAuth access token, minted by your own Google app with a Drive scope. It is used once for this request and never stored.
invalid_unlock_xodocunlock_xodocPOST /imports`unlock_xodoc` must be true or false.
google_token_rejectedaccess_tokenPOST /importsGoogle refused the access token, so nothing was created. It may have expired (they last about an hour) or lack a Drive scope. Mint a fresh one and send the request again.
invalid_earliestearliestPOST /imports`earliest` was not an ISO 8601 timestamp.
docusign_not_connectedsourcePOST /importsThis account has not connected DocuSign. The connection is an OAuth sign-in through a browser, so it must be made in Settings first — a key cannot make it.
invalid_jsonPOST /documents, POST /documents/{id}/sendThe body was not parseable JSON.
request_too_largePOST /documents, POST /documents/{id}/sendThe body exceeded the maximum accepted size.
invalid_bodybodyPOST /documentsThe top level, or a nested value expected to be an object, was an array, null, or a scalar.
invalid_limitlimitGET /documents`limit` was not an integer between 1 and 100.
invalid_cursorstarting_after or ending_beforeGET /documentsThe cursor was not one we issued. Cursors are opaque — pass back exactly what `next_cursor` gave you and never construct one.
conflicting_cursorsstarting_afterGET /documentsBoth `starting_after` and `ending_before` were supplied. Send one.
invalid_statusstatusGET /documents, GET /importsThe `status` filter named a status that does not exist. The message quotes the offending value.
invalid_idempotency_keyPOST /documents, POST /documents/{id}/sendThe `Idempotency-Key` header was present but longer than 255 characters.
invalid_titletitlePOST /documents`title` was missing, empty, or longer than 200 characters.
invalid_typetypePOST /documents`type` was not one of the supported document types.
invalid_primary_languageprimary_languagePOST /documents`primary_language` was not `en` or `es`.
invalid_recipientsrecipientsPOST /documents`recipients` was not an array of 1 to 20 items.
invalid_recipient_namerecipients[N].namePOST /documentsA recipient's `name` was missing, empty, or longer than 120 characters.
invalid_channelrecipients[N].channelPOST /documents`channel` was not `email`, `sms`, or `in_person`.
invalid_recipient_emailrecipients[N].emailPOST /documents`email` was not a plausible address, or exceeded 254 characters.
missing_recipient_emailrecipients[N].emailPOST /documents`channel` was `email` but no `email` was given.
invalid_recipient_phonerecipients[N].phonePOST /documents`phone` exceeded 40 characters.
missing_recipient_phonerecipients[N].phonePOST /documentsNo `phone` was given but this recipient needs one — either `channel` was `sms`, or `auth_level` was `l1_email_sms_otp` (the one-time code is texted even when the signing link goes by email).
invalid_recipient_rolerecipients[N].rolePOST /documents`role` was not a string of at most 60 characters.
invalid_sign_orderrecipients[N].sign_orderPOST /documents`sign_order` was not a positive integer.
invalid_auth_levelrecipients[N].auth_levelPOST /documents`auth_level` was not one of the supported values, or was `l1_email_sms_otp` on a recipient whose `role` is `cc` or `approver` — SMS verification is enforced on the signing path only.
invalid_content_messagecontent.messagePOST /documents`content.message` was not a string of at most 2000 characters.
invalid_sign_order_modecontent.sign_order_modePOST /documents`content.sign_order_mode` was not `sequential` or `parallel`.
invalid_expires_in_dayscontent.expires_in_daysPOST /documents`content.expires_in_days` was not an integer between 1 and 365.
document_not_sendablestatusPOST /documents/{id}/sendOnly a draft or in-review document can be sent. A document that is already sent, completed, voided, or expired cannot be sent again — and this is also what a duplicate send looks like if you did not use an `Idempotency-Key`.
no_recipientsrecipientsPOST /documents/{id}/sendThe document has no recipients to send to.
no_signersrecipientsPOST /documents/{id}/sendEvery recipient is copy-only, so the document could never reach completion. At least one must actually sign.
recipient_contact_requiredrecipientsPOST /documents/{id}/sendA recipient is missing the contact detail its channel needs. The message names the recipient and the field.
approver_requires_sequentialsign_order_modePOST /documents/{id}/sendThe roster includes an approver, which only works as a gate in a sequential order. Set `content.sign_order_mode` to `sequential`.
pay_app_unsignedPOST /documents/{id}/sendThe document mirrors a pay application whose signature block is still blank. Sign the pay app first.

not_foundHTTP 404

No such resource is visible to your account.

This deliberately does not distinguish 'does not exist' from 'belongs to someone else' — a 404 is what another tenant's document looks like. A malformed id returns 404 rather than 400 for the same reason. Templates are also invisible to v1 and return 404.

CodeparamWhereWhat it means
import_not_foundGET /imports/{id}, GET /imports/{id}/itemsNo import with that id in your account. Nonexistent, malformed, and another tenant's ids are all reported the same way on purpose.
document_not_foundEvery /documents/{id} endpointNo document with that id is visible to your account. Also returned for a malformed id and for templates, which v1 does not expose.

rate_limit_errorHTTP 429

You sent more requests than your plan allows.

Back off for `Retry-After` seconds, then retry. Read `X-RateLimit-Remaining-Burst` and `-Daily` to see which budget ran out. See Rate limits for the per-plan numbers.

CodeparamWhereWhat it means
rate_limit_exceededAny endpointYour account's per-minute or per-day budget ran out. The message names which one.
ip_throttledAny endpoint, before authenticationToo many requests from one IP address, measured before your key is even read. This is an abuse rail against unauthenticated floods, not your plan's limit.

idempotency_errorHTTP 409

This `Idempotency-Key` conflicts with a request we have already seen.

Either you reused a key with a different body (`idempotency_key_reused` — use a fresh key per distinct operation) or an earlier request with the same key is still running (`idempotency_in_progress` — wait and retry the identical request, which will replay the original response).

CodeparamWhereWhat it means
import_already_runningPOST /importsA migration is already running for this DocuSign connection. Wait for it to finish, or poll it — starting a second one would file the same envelopes twice.
idempotency_key_reusedPOST /documents, POST /documents/{id}/sendThis `Idempotency-Key` was already used with a different request body. Keys bind to one exact body — use a fresh key for a different operation.
idempotency_in_progressPOST /documents, POST /documents/{id}/sendAn earlier request with this key has not finished. Retry the identical request shortly; you will get the original response, not a second write.

api_errorHTTP 500

Something failed on our side. Nothing is wrong with your request.

Retry with backoff. These are also returned as 502 when a downstream dependency is the cause. Quote the `request_id` when contacting support — it is the only way we can find your exact request in our logs.

CodeparamWhereWhat it means
google_unavailablePOST /importsGoogle Drive did not answer when we checked the token, so nothing was created. Retry shortly.
import_not_startedPOST /importsThe import could not be started. Nothing was queued or downloaded; retry is safe, and a Drive or Dropbox file that did land is never imported twice.
imports_unavailableGET /imports, GET /imports/{id}, GET /imports/{id}/itemsThe imports could not be read. Nothing changed; retry is safe.
account_unavailableGET /accountWe could not load your account record. Retry.
documents_unavailableGET /documentsThe document query failed. Retry.
document_unavailableGET /documents/{id}, GET /documents/{id}/recipientsWe could not load the document. Retry.
recipients_unavailableGET /documents/{id}/recipientsWe could not list the recipients. Retry.
document_create_failedPOST /documentsThe draft could not be written. Nothing was created, so retrying is safe — reuse the same `Idempotency-Key`.
document_send_failedPOST /documents/{id}/sendThe send failed for an internal reason. Retry with the same `Idempotency-Key`.
idempotency_store_failedPOST /documents, POST /documents/{id}/sendWe could not record the idempotency key, so we refused to run the write rather than risk doing it twice. Retry.
key_lookup_failedAny endpointWe could not verify your key. Retry.
tenant_resolution_failedAny endpointWe could not resolve the account for your key. Retry.
tenant_divergenceAny endpointAn internal consistency check on account scoping failed and the request was refused. This is a safety stop, never a data leak. Contact support with the `request_id`.
internal_errorAny endpointAn unhandled internal failure. Contact support with the `request_id`.

Why every bad key returns the same message

A missing, malformed, revoked, expired, or simply wrong key all return an identical 401. That is deliberate: if the responses differed, the endpoint could be used to work out which keys exist. For the same reason, a document belonging to another account returns 404, not 403.

Errors · XOsign API docs