2xx response uses the same application/json envelope:
codeis a stable, machine-readable identifier. Branch your integration on this value. It never changes meaning, so it is safe to compare against.detailis a human-readable sentence meant for logs and debugging. Treat it as informational only: the wording can change at any time, so never match on it.errorsis an array of field-level problems, present mainly oninvalid_request. Each entry has afield(the parameter that failed), acode(a short reason), and amessage(a human explanation). It is an empty array when no single field is at fault.doc_urllinks to this page, anchored on the section for the returnedcode, so a logged error carries a clickable reference to its documentation.
HTTP status codes
The HTTP status reflects the broad category of the failure, whilecode identifies the exact case:
- 4xx: the request was rejected as sent. Fix the request before retrying (bad input, missing or invalid key, resource not found).
- 5xx: the request failed on our side. It is safe to retry, ideally with backoff.
Handle unknown codes gracefully
This catalog grows over time. We may add newcode values, and new endpoints may return existing codes in new places. Treat the set as open: branch on the codes you know, and fall back to handling anything unrecognized by its HTTP status (for example, retry on 5xx, surface a generic message on 4xx) instead of failing.
Error codes
invalid_request
HTTP 400 The request was rejected during validation. Theerrors array lists each failing field with a reason, so you can map the failure back to a specific parameter.
Raised by: GET /v1/invoices · POST /v1/invoices · POST /v1/invoices/validate · POST /v1/number_series · GET /v1/number_series · POST /v1/customer_portal/sessions · POST /v1/tax_ids/validate.
unauthorized
HTTP 401 Authentication failed: the API key is missing, malformed, or revoked. Send a valid key as a bearer token in theAuthorization header. Each key is bound to a single environment (test or live).
no_active_subscription
HTTP 402 Live invoice issuance requires an active subscription. Live keys receive this code until the company has one. A company that has not finished its live-mode onboarding receivesaeat_mandate_not_approved instead, because that check runs first. Test-environment keys are unaffected. The subscription is checked before anything is persisted, so nothing is created. The company subscribes in the dashboard, so do not retry on a timer. Retry once the company confirms its plan is active. On POST /v1/invoices that retry can reuse the same Idempotency-Key. This code releases the key for re-execution instead of replaying the refusal for the rest of the 24-hour window.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
aeat_mandate_not_approved
HTTP 403 Live invoice issuance requires two things of the company, and this code covers both. Its AEAT representation mandate must be approved. The company submits the documentation in the dashboard, and Finseed reviews and approves it. This is a human review and can take days. The company must also have finished live-mode onboarding, which happens when its billing is set up. A company that meets only one of the two still receives this code, notno_active_subscription. This gate does not apply to test-environment keys, and neither does the subscription gate: test invoices are never sent to AEAT. A test key still enforces payload, tax, number series, recipient and duplicate-id rules. Both conditions are checked before anything is persisted, so nothing is created. Do not retry on a timer. Retry once the company confirms in the dashboard that it can issue live invoices. On POST /v1/invoices that retry can reuse the same Idempotency-Key. This code releases the key for re-execution instead of replaying the refusal for the rest of the 24-hour window.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invoice_not_found
HTTP 404 No invoice matches the supplied identifier in the environment the API key belongs to. An invoice created with a live key is not visible to a test key, and vice versa. Raised by:GET /v1/invoices/{id} · GET /v1/invoices/{id}/download · GET /v1/invoices/{id}/file.
number_series_not_found
HTTP 404 No number series matches the supplied identifier in the environment the API key belongs to. A number series created with a live key is not visible to a test key, and vice versa. Raised by:GET /v1/number_series/{id} · DELETE /v1/number_series/{id}.
customer_ambiguous
HTTP 409 The environment has more than one integration, soexternal_customer_id on its own does not identify a single customer (the same id can exist in two integrations for different customers). Supply integration_id alongside external_customer_id to scope the session to one integration. You do not need it when the environment has a single integration. Find the id in the Finseed app under Configuración > Integraciones: open the integration and copy the “ID de integración” shown below its name. See Create a customer portal session for the full parameter reference.
Raised by: POST /v1/customer_portal/sessions.
external_id_already_used
HTTP 409 Theexternal_id on the request already names an invoice in the environment the API key belongs to, so issuing again would duplicate it. This is the business-level duplicate guard: unlike an Idempotency-Key it never expires, and it applies even when the request body differs from the original. The detail names the existing invoice. An external id stays taken after its invoice is voided; to issue a genuinely new invoice, send a new external_id.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
idempotency_conflict
HTTP 409 The suppliedIdempotency-Key cannot be honored. This happens in three situations: the key was already used with a different request body, the original request with this key is still being processed, or the original request ended in an unknown state. The response does not say which. If the original is still running, retrying with the same key and body once it completes replays its response. If you changed the body on purpose because it is a different document, send it with a fresh key. In every other case do not send a fresh key until you know whether the original landed. A fresh key is safe only when the request carries a duplicate guard: an external_id on an invoice, where a duplicate is rejected with external_id_already_used, or the prefix on a number series while that series exists, where a duplicate is rejected with number_series_prefix_taken. Without one, look for the original result or contact support. The key stays blocked for up to 24 hours; a retry after that runs as a new request and can duplicate the original. See Idempotency for the full recovery steps.
Raised by: POST /v1/invoices · POST /v1/number_series.
number_series_in_use
HTTP 409 The number series cannot be deleted because invoices have been issued on it or an integration is configured to use it. A series is immutable once created (there is no update endpoint), so the way to change numbering is to create a new series and issue future invoices on it. A series referenced by an integration can be deleted after the integration is pointed elsewhere. Raised by:DELETE /v1/number_series/{id}.
number_series_numbering_collision
HTTP 409 The prefix of the series being created would compose invoice numbers byte-identical to those of a series that already exists in the environment. This is only possible against a yearly series, which carries the calendar year inside the number: a yearly series with prefixF numbers invoices F-2026-1, so a plain series whose prefix is literally F-2026 would issue the same strings. Pick a prefix that does not end in a hyphen followed by four digits.
Raised by: POST /v1/number_series.
number_series_prefix_taken
HTTP 409 The prefix of the series being created is already taken in this environment. Prefixes are normalized before the uniqueness check (whitespace is removed and letters are uppercased), so two prefixes that only differ in casing or spacing are the same prefix. Pick a different prefix, or reuse the existing series. Raised by:POST /v1/number_series.
download_link_expired
HTTP 410 Thedownload_url returned by the download endpoint has expired or was altered. These links are time-limited bearer URLs; request a new one from the download endpoint rather than storing them indefinitely.
Raised by: GET /v1/invoices/{id}/file.
downgrade_to_simplified_not_allowed
HTTP 422 The rectificativa declarestype: "simplified" while the corrected invoice is complete. Dropping the recipient identification of an already identified invoice is not permitted, so send type: "complete" instead.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
external_reference_invalid
HTTP 422 The external original reference is malformed: thenumber exceeds 60 characters or contains forbidden characters (", ', <, >, =), or issued_on is not a valid calendar date.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
external_reference_matches_existing_invoice
HTTP 422 The external number matches an invoice that was actually issued through Finseed. Rectify it as a local invoice withrectifies.invoice_id; rectifies.external is only for originals that never existed in Finseed.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
family_balance_below_zero
HTTP 422 A rectificativa por diferencias adjusts the original by a delta. This delta would make the accumulated total of the correction chain, including taxes, negative, which is not a valid invoice. Reduce the delta. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
insufficient_recipient_data
HTTP 422 The recipient block does not meet the legal minimums for a complete invoice (for example a too-short name or tax id, or an unrecognized country code). Fix the flagged recipient fields or issue a simplified invoice when no identified recipient exists. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invalid_number_series
HTTP 422 Thenumber_series_id in the request body cannot be used to issue this invoice: it is malformed, belongs to another environment, or names a rectifying series (reserved for corrective invoices). List the usable series with the Number Series endpoints.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invalid_recargo_equivalencia
HTTP 422 Therecargo_equivalencia flag requires every line to carry exactly one vat tax whose rate has a legal surcharge pairing (21%→5.2%, 10%→1.4%, 4%→0.5%). Lines with other tax types or unpaired VAT rates cannot be invoiced under this regime.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invalid_rectifies_reference
HTTP 422 Therectifies block did not name exactly one original to correct. Supply either invoice_id for an invoice issued through Finseed, or external for one issued elsewhere — not both, and not neither.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invalid_tax_data
HTTP 422 The combination of tax types, rates and computed amounts is one the tax agency rejects (for example a positive rate that yields a zero tax amount on a non-zero line), or a line references a tax with no classification in your tax catalog. Review the tax on each line, and classify any pending tax. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invalid_tax_id
HTTP 422 The recipient tax id failed just-in-time validation. Either the format is invalid for its type, or the register did not recognize the id: a Spanish NIF is looked up in the AEAT census together with the supplied name, and an EU VAT number in VIES by number only. Verify the id, and for a Spanish NIF use the name the recipient is registered under. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invalid_tax_rate
HTTP 422 A line item carries a tax rate the tax agency (AEAT) would reject for that tax type on the current date, a rate on a rate-free type, or a missing rate on a rated type. Spanish VAT (vat) currently accepts 0, 4, 10 and 21 percent for invoices issued today; historical temporary rates (2, 5, 7.5) only applied to past operation windows.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invoice_already_replaced
HTTP 422 The referenced invoice has already been fully replaced by a rectificativa por sustitución. Only the active head of the correction chain can be rectified; rectify the replacement instead. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invoice_already_voided
HTTP 422 The referenced invoice was voided (anulada). A voided invoice is legally cancelled, so it cannot be corrected with a rectificativa. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invoice_has_live_canje
HTTP 422 The invoice referenced byrectifies.invoice_id, or the original its correction chain adjusts, was swapped for a complete invoice, called a factura de canje (AEAT invoice type F3), and that exchange invoice has not been voided. An exchange is not a correction. Spanish law keeps the swapped invoice fully valid, and AEAT asks you not to void it because of the exchange. AEAT does allow a swapped invoice to be corrected later, with a rectificativa followed by a new exchange invoice, but Finseed does not support that flow yet. The exchange invoice has to be voided before this chain can be corrected.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invoice_is_canje
HTTP 422 The invoice referenced byrectifies.invoice_id is a factura de canje (AEAT invoice type F3), a complete invoice issued in exchange for one or more simplified invoices. An exchange invoice is not a rectificativa, and a rectificativa does not correct one. AEAT corrects a wrong exchange invoice with two further exchange invoices: the first repeats the wrong data with negative amounts, the second carries the correct data. Finseed does not support that flow yet.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
invoice_not_family_tip
HTTP 422 The referenced invoice is not the active head of its correction chain: a later rectificativa already corrects it. Rectify the current head of the chain instead. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
invoice_not_rectifiable_at_aeat
HTTP 422 The referenced invoice is not in a state the tax agency (AEAT) allows a rectificativa against: its registration failed, was skipped, or never happened. Only invoices that registered successfully can be rectified. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
num_series_not_applicable_for_order_invoice
HTTP 422 A rectificativa against an invoice created from an order always numbers on the series its integration owns for that kind of correction (which depends on the integration and on whether the correction is simplified or complete). Omitnumber_series_id for these; supply it only for a rectificativa against a standalone or external original.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
reason_incompatible_with_recipient
HTTP 422 The chosenreason restricts which recipient tax id types are allowed. insolvency accepts a Spanish NIF, an EU VAT number, or an unregistered recipient; uncollectible accepts a Spanish NIF or an unregistered recipient. The recipient on this invoice does not meet that restriction.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
recipient_overrides_not_allowed_for_differences
HTTP 422 A rectificativa por diferencias inherits its recipient from the corrected invoice, so arecipient block cannot be supplied. Omit it, or issue a rectificativa por sustitución (method: "substitution") when the recipient must change.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
rectified_amounts_required_for_substitution
HTTP 422 When replacing an external original by substitution, the tax agency requires the original amounts (ImporteRectificación), which Finseed cannot compute because the original never existed here. Supply rectifies.external.rectified_amounts.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
rectified_invoice_not_found
HTTP 422 Therectifies.invoice_id does not match any invoice in the environment the API key belongs to. Use the opaque inv_ identifier of an invoice issued through Finseed; to correct an invoice issued elsewhere, use rectifies.external instead.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
rectifying_series_invalid
HTTP 422 A rectificativa must be issued on a rectifying series. Thenumber_series_id is malformed, belongs to another environment, or names a standard series. List your rectifying series with the Number Series endpoints, or omit it to use the environment default.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
replacement_total_negative
HTTP 422 A rectificativa por sustitución restates the full invoice, so its net total cannot be negative. Adjust the line items so the total is at or above zero. Raised by:POST /v1/invoices · POST /v1/invoices/validate.
simplified_amount_exceeded
HTTP 422 Spanish law caps simplified invoices at 3,000 EUR including tax. Above that amount the recipient must be identified: sendtype: "complete" with a full recipient block.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
simplified_incompatible_tax_type
HTTP 422 The invoice combinestype: "simplified" with a tax type that legally requires an identified recipient, such as es_inversion_sujeto_pasivo (reverse charge). Issue a complete invoice for these operations.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
tax_id_type_country_mismatch
HTTP 422 The suppliedtax_id_type and tax_id_country contradict each other. Each type constrains the country: es_nif and not_registered require ES; eu_vat requires an EU country other than ES; official_document, residence_certificate and other require a country other than ES; passport accepts any country. Fix the pair, or omit tax_id_country when the type implies it (or omit both to auto-detect them from the tax id).
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
upgrade_to_complete_not_allowed
HTTP 422 The rectificativa declarestype: "complete" while the corrected invoice is simplified. A rectificativa cannot add a recipient to a simplified invoice. The instrument that issues a complete invoice for a simplified one is a factura de canje (AEAT invoice type F3). It is offered in the Finseed app for an original invoice created directly in Finseed, through this API or the app, that is not itself a rectificativa and is still the only active invoice of its correction chain; this API does not offer it yet. An invoice generated from an integration order is not exchanged that way: it becomes complete automatically once the order carries the recipient tax data.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
rate_limited
HTTP 429 The per-key rate limit for this endpoint was exceeded. The response carries aRetry-After header with the seconds to wait. Spread out bursts or batch your work; sustained limits are documented per endpoint.
Raised by: POST /v1/invoices/validate · POST /v1/tax_ids/validate.
internal_error
HTTP 500 An unexpected error occurred on our side. No partial work is persisted, so the request can be retried safely.invoice_pending_at_aeat
HTTP 503 The invoice being corrected has not finished registering at the tax agency (AEAT). A rectificativa can only reference an invoice that is already registered. Nothing is persisted. This is a timing condition, not a rejection of the request. Registration usually completes within minutes, with no action on your side. Wait for the interval in theRetry-After header, then retry. You can also poll the invoice until its verifactu.status leaves pending. On POST /v1/invoices, the retry can reuse the same Idempotency-Key. This status releases the key for re-execution instead of replaying the failure. If registration failed rather than completing, the retry returns invoice_not_rectifiable_at_aeat.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.
recipient_changed_during_validation
HTTP 503 A correction that restates the recipient resolves which recipient it would file before it claims the write lock on the invoice. If a concurrent operation (such as a subsanacion) changes the corrected invoice’s tax id type or country during that window, the recipient resolved earlier no longer describes the invoice, so the correction is refused and nothing is persisted. This is a lost race rather than a rejection of the request. Retry; sending the sameIdempotency-Key is safe, because this status releases the key for re-execution instead of replaying the failure. The retry resolves and checks the recipient again, so it may succeed or return a fresh validation error if the changed tax data is not valid for its new type or country.
Raised by: POST /v1/invoices.
tax_id_validation_unavailable
HTTP 503 Complete invoices check a Spanish NIF against the AEAT census, and an EU VAT number against VIES, at creation time. When that external service is down, creation fails with this code and nothing is persisted. Retry later; onPOST /v1/invoices, sending the same Idempotency-Key makes the retry safe. The remaining id types consult no external register, so this code cannot arise for them.
Raised by: POST /v1/invoices · POST /v1/invoices/validate.