Error codes
The stable code values, what causes them, and what to do.
Failures come back as:
{ "error": { "code": "VALIDATION_FAILED", "message": "…" } }code is stable and is what your software should branch on. message is written for people and its wording may change.
The document itself
| Code | HTTP | Cause |
|---|---|---|
PEPPOL_VALIDATION_FAILED | 400 | The document does not satisfy the Peppol rules for its type |
VALIDATION_FAILED | 400 | The request was malformed: a bad field, an unknown JSON key, a cursor that does not parse |
INVALID_JSON | 400 | The body is not JSON at all, or not the shape the route expects |
VALIDATION_UNAVAILABLE | 400 / 503 | The document could not be checked. Retry later; it is not a verdict |
PEPPOL_DOCUMENT_NOT_SUPPORTED | 400 | No capability covers this document type in this direction |
PEPPOL_RECIPIENT_NOT_FOUND | 400 | The receiving address is not known to us |
PEPPOL_DISCOVERY_FAILED | 400 / 503 | The recipient could not be looked up on the network |
PEPPOL_LOCAL_PARTICIPANT | 400 | The sender is a local participant — a sandbox company's, for instance — which is not published on the network and cannot send onto it |
PAYLOAD_TOO_LARGE | 413 | Over the size limit for the body or the document |
VALIDATION_ERROR | 502 | POST /v1/validate only: the check ran but its answer could not be read. Not your document's fault, and retrying will not change it |
A validation failure is yours to fix — resending the same bytes fails again. A discovery failure is often the recipient's address being wrong, or their registration not covering the document type.
Credentials and access
| Code | HTTP | Cause |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed or revoked credential |
FORBIDDEN | 403 | The credential is valid but not allowed here |
PERMISSION_DENIED | 403 | The API key lacks the permission this route needs |
PASSWORD_CHANGE_REQUIRED | 403 | The user must set a new password first |
ACCOUNT_CLOSED / COMPANY_CLOSED | 403 | The credential belongs to an account or company that has been closed |
OPERATOR_MANAGED | 400 | Changing an SFTP login's method, status or company is done for you, not over the API; you can only issue a new password |
NOT_FOUND | 404 | Missing, out of your scope, or not a valid id |
NOT_FOUND is deliberately returned for things that exist but are outside your scope. A credential confined to one company cannot tell the difference between another company's document and one that was never created — that is the point.
State and conflicts
| Code | HTTP | Cause |
|---|---|---|
CONFLICT | 409 | It already exists |
INVALID_STATE | 409 | The action does not apply in the current lifecycle state |
IDEMPOTENCY_CONFLICT | 409 | The same Idempotency-Key with a different body |
IDENTIFIER_TAKEN | 409 | That company or participant identifier is in use |
IDENTIFIER_IN_USE | 409 | The identifier cannot change while an SFTP login uses it as its directory |
EMAIL_CONFLICT | 409 | That email already belongs to a user |
COMPANY_REQUIRED | 400 | Your credential reaches several companies and the request did not name one |
ACCOUNT_CLOSED / COMPANY_CLOSED | 409 | Closed, and not accepting changes |
LAST_OWNER / LAST_API_KEY | 409 | Removing it would leave no way in |
COMPANY_NOT_DEREGISTERED | 409 | A company can only be deleted once every participant under it is DEACTIVATED |
NOT_SANDBOX | 409 | Simulated inbound documents are only for companies created with sandbox: true |
Company verification (KYC)
| Code | HTTP | Cause |
|---|---|---|
KYC_USE_ONBOARDING_LINK | 403 | API keys cannot do a company's KYC. Issue an onboarding link and hand it to the company |
KYC_LOCKED | 409 | The company is verified; its registration number and country can no longer be changed over the API |
KYC_NOT_CONFIGURED | 501 | Verification is not offered here |
KYC_ONBOARDING_LINK_INVALID | 400 | The onboarding link is unknown, replaced by a newer one, or expired |
KYC_SIGNING_LINK_INVALID | 400 | The signing link is unknown, used, declined or expired |
KYC_DOCUMENT_NOT_SEALED | 400 | The register extract is not the sealed original — a scan, a print or a re-saved copy |
KYC_DOCUMENT_NOT_FROM_REGISTER | 400 | The extract was not issued by the company register |
KYC_DOCUMENT_TOO_OLD | 400 | The extract is older than 90 days |
KYC_ORG_NUMBER_MISMATCH | 400 | The extract is for a different registration number than the company's |
KYC_DOCUMENT_UNREADABLE | 400 | The extract could not be read |
KYC_MANUAL_REQUIRED | 400 | The company's country has no register extract to upload; enter its details instead |
KYC_SIGNATORIES_INSUFFICIENT | 400 | The chosen signers cannot bind the company together |
KYC_NO_AGREEMENT | 409 | There is no agreement published to sign. Contact support |
Rate limiting and availability
| Code | HTTP | Cause |
|---|---|---|
RATE_LIMITED | 429 | Too many requests. Wait for the number of seconds in Retry-After |
PEPPOL_UNAVAILABLE | 503 | A dependency is down. Retry with backoff |
PEPPOL_INTERNAL_ERROR | 500 | Our fault. Retrying an identical request rarely helps |
Signing in to the console
These come from the sign-in and account-security routes the console uses. An integration working with an API key does not meet them.
| Code | HTTP | Cause |
|---|---|---|
RESET_TOKEN_INVALID | 400 | The password-reset or invitation link is unknown, expired or already used |
CURRENT_PASSWORD_REQUIRED / CURRENT_PASSWORD_INVALID | 400 | Changing your own password needs your current one, and it was missing or wrong |
PASSWORD_CHANGE_NOT_ALLOWED_FOR_SSO_SESSION | 403 | You signed in with single sign-on, so there is no password here to change |
SSO_NOT_LINKED | 401 | That single sign-on identity is not linked to a user |
SSO_EMAIL_MISMATCH | 400 | The identity's email address does not match the user's |
PASSKEY_REJECTED | 400 | The passkey could not be verified |
PASSKEY_CHALLENGE_EXPIRED | 400 | The passkey challenge expired or was already used; start over |
PASSKEY_CLONE_DETECTED | 401 | The authenticator reported a counter that went backwards — the passkey may have been copied |
PASSKEY_ALREADY_REGISTERED / PASSKEY_LIMIT | 409 | This authenticator already has a passkey here, or the user has as many as allowed |
PASSKEYS_NOT_CONFIGURED | 501 | Passkeys are not offered here |
What to retry
Retry 429 RATE_LIMITED after Retry-After, and 503 with exponential backoff. Both are temporary by definition.
Do not retry a 4xx unchanged — the answer will not differ. The exception is VALIDATION_UNAVAILABLE, which says the check could not be run rather than that it failed.
Always carry an Idempotency-Key on sends, so that a retry after a timeout — where you never learned whether the first attempt landed — cannot produce a second document.