Error codes
Every code the API can put in ErrorResponse.code, grouped by the area that produces it. Branch
on the code, not on the message. The envelope itself is described in
Conventions.
Two spellings exist. Codes that travel through the central exception handler are
snake_case. Codes that a controller returns directly, which is the connector, e-Factura, and
source-sync families, are kebab-case. Both are stable, and neither converts into the other.
Generic
These are the fallbacks. A specific code is preferred wherever one exists, so seeing a generic one means no rule claimed the failure.
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
not_found |
404 | web.errors.not_found |
The row is absent, or it exists somewhere this credential cannot see | Check the id and the client company. Do not retry unchanged |
forbidden |
403 | web.errors.forbidden |
The credential is valid but not for this organization, client, or action | Check the role and the per-client permission |
validation_failed |
400 | web.errors.validation_failed |
A field is missing, malformed, or out of range | Read errors for the field names, fix, and resend |
conflict |
409 | web.errors.conflict |
Current state refuses a well-formed request | Re-read the resource, then decide. Retrying unchanged repeats it |
internal_error |
500 | web.errors.unexpected |
Something failed inside the API | Retry with backoff. Quote the correlation id if it persists |
forbidden also carries web.errors.organization_access_denied as its message key when the
organization header names an organization the caller is not a member of.
Authentication and scope
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
api_key_invalid |
401 | web.errors.api_key_invalid |
The key is unknown, revoked, or expired. Which one is not disclosed | Check the key, then create a new one |
organization_required |
403 | web.errors.organization_required |
A session request reached an organization-scoped route without X-Organization-Id |
Send the header. API keys never need it |
api_key_scope_escalation |
403 | web.errors.api_key_scope_escalation |
A key creation asked for a permission the creator does not hold | Ask only for scopes you already have |
See Authentication.
Entitlements and the paywall
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
entitlement_product_access_required |
402 | web.errors.entitlement.productAccessRequired |
The organization has no active package and the route is not on the allowlist | Read GET /v1/billing, then pay |
entitlement_upgrade_required |
403 | web.errors.entitlement.upgradeRequired |
The package is active but too small for this surface | Change plan |
entitlement_collection_paused |
403 | web.errors.entitlement.collectionPaused |
Collection is paused for this organization | Restore billing, then retry |
The public API itself needs at least the 25-client package. MCP needs at least the 5-company
package. Both refuse with entitlement_upgrade_required.
See Conventions and Billing.
Billing
Every code here answers 409.
| Code | Message key | When it happens | What to do |
|---|---|---|---|
billing_no_customer |
web.errors.billing.noCustomer |
The organization has no billing customer yet | Start a checkout instead of opening the portal |
billing_unknown_price |
web.errors.billing.unknownPrice |
The priceLookupKey is not in the price book |
Read the valid keys off GET /v1/billing |
billing_gateway_unavailable |
web.errors.billing.gatewayUnavailable |
Stripe could not be reached | Retry with backoff |
billing_return_url_rejected |
web.errors.billing.returnUrlRejected |
The return, success, or cancel URL is not on the allowlist | Use a URL on your own configured origin |
billing_no_subscription |
web.errors.billing.noSubscription |
There is no subscription to change | Start a checkout |
billing_same_plan |
web.errors.billing.samePlan |
The plan asked for is the plan already running | Nothing. It is already the plan |
billing_annual_term_running |
web.errors.billing.annualTermRunning |
An annual term is mid-flight. details.currentPeriodEnd says until when |
Wait for the date, or write to sales@syllevo.com |
Provisioning
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
provisioning_unpaid_organization_exists |
409 | web.errors.provisioning.unpaidOrganizationExists |
Creating another organization while one already sits unpaid | Pay for the existing one, or delete it, then retry |
It is 409 rather than 402 on purpose. The refusal is about the organization you already have, not about the one you are creating.
Team, roles, and invitations
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
member_self_grant_change |
403 | web.errors.member_self_grant_change |
A member tried to change their own grants | Ask a colleague with the role to do it |
member_role_not_below_caller |
403 | web.errors.member_role_not_below_caller |
The target holds a role at or above the caller's | Only an owner can act on an admin |
member_role_not_allowed |
403 | web.errors.members.roleNotAllowed |
The new role is one the caller may not assign | An owner must do it |
member_owner_protected |
403 | web.errors.members.ownerProtected |
The target is the owner | Transfer ownership first |
invitation_role_not_allowed |
403 | web.errors.invitations.roleNotAllowed |
The invitation asks for a role the caller may not grant | Invite at a lower role |
invitation_not_found |
404 | web.errors.invitations.notFound |
No such invitation, for this organization or for this person | Refresh the list |
invitation_already_member |
409 | web.errors.invitations.alreadyMember |
The address already belongs to a member | Nothing. They are already in |
invitation_not_pending |
409 | web.errors.invitations.notPending |
It was already accepted, declined, or revoked | Send a new invitation |
invitation_expired |
409 | web.errors.invitations.expired |
Accepting an invitation that is past its expiry | Ask the firm to resend it, which puts the clock back |
See Team, roles, and invitations.
Documents and review
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
document_original_missing |
409 | web.errors.document_original_missing |
The row exists but the stored file behind it does not | Not retryable. Re-signing points at the same absent key. Report it |
review_possible_duplicate |
409 | web.errors.review.possibleDuplicate |
Confirming would file a second copy of an invoice already on file | Compare the two, then reject or merge |
review_incomplete_reading |
409 | web.errors.review.incompleteReading |
Confirming a reading that is missing required fields | Send corrected fields with the decision |
review_unidentifiable_supplier |
409 | web.errors.review.unidentifiableSupplier |
The supplier on the document could not be identified | Correct the supplier fields, then confirm |
payload_too_large |
413 | web.errors.payload_too_large |
The body is over the route's limit | Split the upload. See size limits |
See Documents.
Sales invoices and idempotency
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
invoice_number_exists |
409 | web.errors.invoice_number_exists |
A different sales invoice already has this number and issue date | Look at details.existingInvoiceId. An issued invoice is never replaced |
sales_invoice_file_in_use |
409 | web.errors.sales_invoice_file_in_use |
One of the files is already a collected purchase or another sale | Send the right file for this sale |
sales_invoice_in_spv |
409 | web.errors.sales_invoice_in_spv |
Trashing a file of a sale that is on its way to SPV, or that ANAF accepted | Nothing. It stays on file with ANAF's signed answer |
sales_invoice_metadata_mismatch |
400 | web.errors.sales_invoice_metadata_mismatch |
A field you sent disagrees with the XML | Fix the field named in details, or leave it out and let the XML speak |
sales_invoice_xml_unreadable |
400 | web.errors.sales_invoice_xml_unreadable |
The XML is not a UBL or CIUS-RO invoice, or lacks its number, date, currency, or total | Send the XML your invoicing software exported, unchanged |
sales_invoice_xml_unreadable |
400 | web.errors.sales_invoice_xml_field_invalid |
The XML reads fine but states a value no invoice can hold, such as a buyer country ROU |
Fix the field named in errors and details.field in your invoicing software |
sales_invoice_pdf_missing |
422 | web.errors.sales_invoice_pdf_missing |
Asking to email a sales invoice that was imported without a PDF | Email it from your invoicing software. Syllevo sends only the PDF |
idempotency_key_required |
400 | web.errors.idempotency_key_required |
The route needs an Idempotency-Key header and none usable came |
Send one. See idempotency |
idempotency_key_reused |
422 | web.errors.idempotency_key_reused |
This key was already used for a different request | Use a new key for a new request |
idempotency_request_in_progress |
409 | web.errors.idempotency_request_in_progress |
The first request with this key has not finished | Wait a couple of seconds and retry with the same key |
Sending sales invoices to SPV
These answer POST /v1/sales-invoices/{invoiceId}/submission, the submissionError on an import
sent with submit=true, and the transmission consent switch. The same codes can come back later as
a submission's lastErrorCode, when a check that passed at request time fails right before the
upload. See Invoices.
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
spv_transmission_disabled |
403 | web.errors.spv_transmission_disabled |
Sending sales invoices to SPV is off for the organization | Write to sales@syllevo.com to have it switched on |
spv_consent_missing |
403 | web.errors.spv_consent_missing |
The business hasn't allowed Syllevo to send its sales invoices | The person who connected e-Factura turns on the transmission consent |
spv_consent_not_connection_holder |
403 | web.errors.spv_consent_not_connection_holder |
Turning the consent on by anyone but the person who connected the business to e-Factura | That person does it, signed in, whatever their role |
spv_submission_exists |
409 | web.errors.spv_submission_exists |
The invoice is already on its way, or ANAF already accepted it. The body carries submission |
Nothing. Read submission for where it stands |
spv_already_in_spv |
409 | web.errors.spv_already_in_spv |
The invoice already arrived from the SPV sent list, so the business's own software sent it | Nothing. It is in SPV |
spv_xml_missing |
422 | web.errors.spv_xml_missing |
The invoice has no XML file, or submit=true came with no xml part |
Import the XML your invoicing software exported |
spv_xml_invalid |
422 | web.errors.spv_xml_invalid |
The XML isn't a UBL invoice or credit note in CIUS-RO. details.reason says which part failed |
Export it again as e-Factura XML from your invoicing software |
spv_seller_mismatch |
422 | web.errors.spv_seller_mismatch |
The seller's CUI in the XML isn't the business's CUI | Check it was imported into the right business |
spv_not_enrolled |
422 | web.errors.spv_not_enrolled |
The business has no working e-Factura connection or enrollment for its CUI | Connect or reconnect e-Factura, then send again |
spv_cui_not_authorized |
422 | web.errors.spv_cui_not_authorized |
ANAF says the connected certificate has no SPV rights for this CUI | Fix the rights in ANAF, then send again |
The reasons a submission fails after it was accepted for sending (validation_failed, rejected,
outcome_unknown, and the rest) are not HTTP errors. They are listed in
Invoices.
See Invoices.
Suppliers
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
supplier_merge_same_supplier |
400 | web.errors.suppliers.mergeSameSupplier |
Both halves of the merge are the same supplier | Pick a different target |
supplier_merge_profile_mismatch |
409 | web.errors.suppliers.mergeProfileMismatch |
The two suppliers belong to different client companies | Merge only within one client |
supplier_merge_official_supplier |
409 | web.errors.suppliers.mergeOfficialSupplier |
The supplier being removed is the directory-backed official row | Merge the other way round |
supplier_merge_already_official |
409 | web.errors.suppliers.mergeAlreadyOfficial |
The supplier is already the directory entry it was aimed at | Nothing. It is already merged |
See Suppliers.
Sources and syncs
Returned directly by the source routes, so these are kebab-case.
| Code | Status | When it happens | What to do |
|---|---|---|---|
account-not-found |
404 | No such source on that client company | Check the id and the client |
account-sync-not-pullable |
409 | The source has nothing to pull on demand, for example a pure inbox | Nothing. It receives rather than fetches |
account-sync-too-soon |
409 | It was checked too recently. details.availableAt says when it reopens |
Wait until availableAt |
source_cap_reached |
409 | The organization is at its source limit | Disconnect an unused source, or change plan |
See Sources.
Supplier connectors
Returned directly by the connector routes. connector-not-found answers 404 and every other code
here answers 409. Every one of them can also turn up as lastErrorCode on a connector account
after a background poll, where it describes the last thing that went wrong rather than the answer
to your request.
| Code | When it happens | What to do |
|---|---|---|
connector-not-found |
No connector with that slug | Read GET /v1/connectors for the live catalog |
connector-not-configured |
The connector exists but this deployment has no credentials for it | Write to sales@syllevo.com to have it switched on. Nothing to change on your side |
connector-state-invalid |
The callback's state value did not check out or has expired | Start the connect again |
connector-approval-pending |
The supplier has not approved the access request yet | Wait, then retry. Approval is on the supplier's side |
connector-approval-refused |
The supplier refused the access request | The client has to sort it out with the supplier |
connector-already-connected |
This account is already connected on this client company | Nothing. It is connected |
connector-supplier-unavailable |
The supplier's own system could not be reached | Retry later |
connector-credential-invalid |
The submitted credentials were not accepted | Check them and resubmit |
connector-credential-expired |
The stored credential has expired | Reconnect |
connector-credential-revoked |
The supplier revoked the credential | Reconnect |
connector-credential-missing |
No credential is stored for this connection | Reconnect |
connector-download-refused |
The supplier refused to serve the invoice file | Retry later, then report it |
connector-download-too-large |
The file is over the ceiling | Fetch it from the supplier directly |
connector-not-a-pdf |
What came back was not a PDF | Report it. The supplier changed something |
connector-mode-mismatch |
Credentials were submitted to a redirect connector, or the reverse | Use the mode GET /v1/connectors reports |
e-Factura and ANAF
Every code here answers 409. The exception is the module gate: when e-Factura is switched off for
the deployment, the routes answer a plain 404 with not_found, and no e-Factura code at all.
That is deliberate. A door that is not there tells nothing about a door that is.
| Code | When it happens | What to do |
|---|---|---|
efactura-state-invalid |
The callback's state could not be opened, expired, or names someone else | Start the connect again |
efactura-exchange-failed |
ANAF could not be reached or answered unusably during the exchange | Start the connect again, with the token in hand |
efactura-connection-not-connected |
The grant named is disconnected, expired, or last failed to refresh | Reconnect before enrolling anything against it |
efactura-cui-invalid |
The fiscal code is not a valid Romanian CUI | Fix the CUI |
efactura-already-enrolled |
That fiscal code is already enrolled on this client company | Nothing. It is enrolled |
efactura-not-configured |
The module is on but the deployment has no ANAF OAuth application | Nothing you can do |
efactura-opaque-token |
ANAF returned a token the exchange could not read | Start the connect again |
efactura-invalid-grant |
ANAF refused the grant | Start the connect again |
efactura_already_connected |
A grant is already connected for this organization. Note the underscores: this one travels through the exception handler | Disconnect first |
Codes you see on a connection rather than in a response
The polling side has its own family. They arrive as lastErrorCode on an account or a connection
and describe why the last sweep failed. They are not HTTP responses.
efactura-not-enabled, efactura-rights-missing, efactura-rate-limited, efactura-clock-skew,
efactura-download-expired, efactura-daily-cap-reached, efactura-not-found,
efactura-invalid-id, efactura-unauthorized, efactura-not-an-archive,
efactura-download-too-large, efactura-http-error, efactura-response-not-json,
efactura-transport-error, efactura-timeout, efactura-unknown-error,
efactura-tokens-unreadable, efactura-download-attempts-exhausted, efactura-zip-malformed,
efactura-token-response-invalid.
Two are worth knowing about because they are terminal rather than transient:
efactura-download-expired. ANAF keeps a message downloadable for 60 days. Past that it is gone and no retry brings it back.efactura-daily-cap-reached. ANAF allows ten downloads per message per day. It clears tomorrow.
See e-Factura and SPV.
Reminders
Every code here answers 409.
| Code | Message key | When it happens | What to do |
|---|---|---|---|
reminder_sequence_not_active |
web.errors.reminders.sequenceNotActive |
Pausing something that is not running | Read the sequence state first |
reminder_sequence_not_paused |
web.errors.reminders.sequenceNotPaused |
Resuming something that is not paused | Read the sequence state first |
reminder_sequence_not_chasing |
web.errors.reminders.sequenceNotChasing |
Sending now while the sequence is not in a chasing state | Resume it, or wait for the next period |
reminder_nothing_missing |
web.errors.reminders.nothingMissing |
There is nothing left to chase for the period | Nothing. The month is complete |
reminder_no_usable_contact |
web.errors.reminders.noUsableContact |
No contact can receive mail, often after a hard bounce | Add or fix a contact |
reminder_touch_already_pending |
web.errors.reminders.touchAlreadyPending |
One is already scheduled. pendingTouchAt says when |
Wait for it, or cancel it first |
reminder_escalation_confirmation_required |
web.errors.reminders.escalationConfirmationRequired |
Escalation was turned on without confirming it | Resend with confirmEscalation set to true |
reminder_touch_already_pending on POST /v1/profiles/{profileId}/reminders/send-now comes back
in the extended refusal body that carries pendingTouchAt. See Reminders.
Affiliates
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
affiliate_code_taken |
409 | web.errors.affiliate_code_taken |
Somebody else already has that code | Pick another |
affiliate_code_limit_reached |
409 | web.errors.affiliate_code_limit_reached |
Too many renames | Keep the code you have |
Upload links
These are per-file results inside the upload response, not HTTP statuses. One bad file does not
fail the batch: the response reports each file separately with accepted, errorCode, and
messageKey.
| Code | Message key | Means |
|---|---|---|
file_empty |
validation.fileRequired |
Zero bytes |
file_too_large |
validation.fileTooLarge |
Over the per-file ceiling |
file_type_not_accepted |
validation.invalidValue |
The type is not one Syllevo accepts |
file_type_mismatch |
validation.fileTypeMismatch |
The declared type and the actual bytes disagree |
file_name_invalid |
validation.invalidValue |
The file name is unusable |
too_many_files |
validation.tooManyFiles |
More files than one submission allows |
not_stored |
web.errors.upload_not_stored |
Storage refused it. Nothing was silently dropped |
See Upload links.
Ingestion
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
ingestion_rejected |
401 | web.errors.ingestion_rejected |
The ingestion signature did not verify | Not your route. See Webhooks |
payload_too_large |
413 | web.errors.payload_too_large |
The message is over the ceiling | Nothing. The sender must split it |
Rate limits and upstreams
| Code | Status | Message key | When it happens | What to do |
|---|---|---|---|---|
rate_limit_exceeded |
429 | web.errors.rate_limit_exceeded |
Over the limit for the window | Wait a minute and retry with jitter |
provider_unreachable |
503 | web.errors.provider_unreachable |
An upstream Syllevo depends on is down | Retry with backoff |
GET /v1/registry/company is the only route that returns 503 today.
MCP
These belong to the MCP surface, not to the REST routes on these pages. They are listed so the inventory is complete.
| Code | Status | Message key | When it happens |
|---|---|---|---|
mcp_organization_required |
403 | web.errors.mcp.organizationRequired |
The tool call named no organization |
mcp_scope_missing |
403 | web.errors.mcp.scopeMissing |
The OAuth grant lacks the scope the tool needs |
mcp_organization_disabled |
403 | web.errors.mcp.organizationDisabled |
The organization has MCP switched off in settings |