Factuarea APIDevelopers

Error handling

Normalized error envelope, type and code catalog with stable anchors, and retry strategy.

Every error response from the public API uses a consistent JSON envelope. The HTTP status indicates the general category; the type field disambiguates and the code field points to the specific cause.

Envelope

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "El campo client_id es obligatorio.",
    "param": "client_id",
    "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA",
    "doc_url": "https://docs.factuarea.com/guides/errors#parameter_invalid"
  }
}

Fields:

  • type — general error category. Stable and enumerated (list below).
  • code — specific cause. Stable and enumerated.
  • subcode — optional. Present where code alone is ambiguous: on 409 duplication conflicts it pinpoints the duplicate key (e.g. subcode: "tax_id_already_exists"), and on 402 payment errors it pinpoints which gate rejected the call (e.g. subcode: "webhooks_addon_required"). Like code, it is stable and invariant across languages and API versions.
  • message — human text in Spanish. Not guaranteed stable across versions; useful for logging and display.
  • param — optional, present in validation errors. Points to the first offending field. On multi-field validation errors the full set lives in errors[] (see below).
  • errors[] — optional, present on 422 validation errors. Lists all failed fields (see Multi-field validation errors).
  • details — optional. Carries existing_resource_id on 409 duplication conflicts (see Duplication conflicts) and payment_setup_url on the 402 errors that need a payment method registered (see payment_required_error).
  • doc_url — optional. Link to this guide with anchor to the specific code (#{code}).
  • request_id — unique request identifier (req_<ULID>). Always include it when contacting support. It is also returned in the X-Request-Id response header.

The error object always carries type, code and message; the remaining fields are present when relevant.

Multi-field validation errors

A 422 validation error reports every failed field, not just the first. The flat param/message still mirror the first field (for backward compatibility), and errors[] carries one item per failed field — so you fix all of them in a single round-trip instead of one request per field.

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_param_value",
    "message": "El campo client_id es obligatorio.",
    "param": "client_id",
    "errors": [
      { "param": "client_id", "code": "parameter_missing", "message": "El campo client_id es obligatorio." },
      { "param": "issue_date", "code": "parameter_invalid_format", "message": "El formato de la fecha no es válido.", "expected_format": "YYYY-MM-DD" },
      { "param": "status", "code": "parameter_invalid_enum", "message": "El valor no es válido.", "allowed_values": ["draft", "sent", "paid"] }
    ],
    "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA",
    "doc_url": "https://docs.factuarea.com/guides/errors#invalid_param_value"
  }
}

Each item of errors[] carries:

  • param — the failed field name.
  • code — a stable machine-readable code derived from the failed validation rule (e.g. parameter_missing, parameter_invalid_format, parameter_invalid_enum, parameter_invalid_integer).
  • message — human-readable description of the field error.
  • expected_format — optional. Present only on format errors; the expected pattern (e.g. YYYY-MM-DD, uuid, email, url).
  • allowed_values — optional. Present only on enum errors; the list of legal values.

errors[] is purely additive — integrations that only read param, code and message keep working unchanged.

Duplication conflicts

A 409 duplication conflict may include details.existing_resource_id when that operation supplies it. Treat those details as optional. Canonical contact fiscal conflicts return resource_already_exists with subcode: tax_id_already_exists; an external-reference conflict can return external_id_already_exists. Neither guarantees an existing-resource UUID in the error.

{
  "error": {
    "type": "conflict_error",
    "code": "resource_already_exists",
    "subcode": "tax_id_already_exists",
    "message": "Ya existe un contacto con esa identidad fiscal en la empresa.",
    "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM",
    "doc_url": "https://docs.factuarea.com/guides/errors#resource_already_exists"
  }
}

If an existing-resource UUID is provided, retrieve it with its documented GET operation. For contacts without that detail, use POST /v1/companies/{company}/contacts/find-by-tax-id or POST /v1/companies/{company}/contacts/find-by-external-id, then review the matched identity before updating or assigning a role. All lookups stay within the company the {company} segment names.

Problem Details (RFC 9457)

Send Accept: application/problem+json to receive the same error as an RFC 9457 Problem Details document with Content-Type: application/problem+json. With Accept: application/json, Accept: */* or no Accept header you get the flat envelope above.

{
  "type": "https://docs.factuarea.com/errors/resource_already_exists",
  "title": "Resource already exists",
  "status": 409,
  "detail": "Ya existe un contacto con esa identidad fiscal en la empresa.",
  "instance": "/v1/companies/{company}/contacts",
  "code": "resource_already_exists",
  "subcode": "tax_id_already_exists",
  "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM"
}
  • type — the documentation page of that specific code, e.g. https://docs.factuarea.com/errors/resource_already_exists. The code is the only variable segment, so you can build and compare the URI yourself. It used to be a single URI shared by every problem; if you match on it, match on code instead — that one never moves.
  • title — a short human summary of the problem type.
  • status — the HTTP status code.
  • detail — the human-readable message.
  • instance — the path of the affected resource.

The problem+json variant does not drop any extension data: code, subcode, param, errors[], details, doc_url and request_id are preserved as RFC 9457 extension members.

Localized messages

The message (and the problem+json detail) is localized via the Accept-Language header for the codes in the stable catalog. Supported languages are es, en and ca, with fallback to es when the header is absent or requests an unsupported language. The code and subcode are invariant across languages — always branch on code, never on message.

Accept-Language: en        → message in English
Accept-Language: ca-ES     → message in Catalan
(absent / Accept-Language: de) → message in Spanish (fallback)

Dynamic messages emitted by domain exceptions stay in Spanish; only the stable catalog messages are localized.

Error types

typeHTTPDescription
invalid_request_error400, 405, 411, 413, 415 or 422Malformed or oversized payload, unsupported method or media type, missing/invalid parameters or business validation failure.
authentication_error401 or 429The API key is missing, invalid, revoked, expired or the IP is not in the allowlist; 429 (too_many_auth_failures) when too many authentication attempts fail.
payment_required_error402A commercial prerequisite is missing: a charge failed, no payment method is configured, there is no plan to bill to, or the current plan grants zero capacity for the operation.
authorization_error403 or 422The key is valid but the operation is not open to it: the scope doesn't cover the endpoint, a test key tries to change a live account, the access level is read-only, the operation belongs to another account, or the company's plan doesn't include the feature. A cap of your plan (keys, webhook endpoints) or a scope it doesn't allow answers 422.
not_found_error404The requested resource doesn't exist or doesn't belong to the key's company.
conflict_error409Creation conflict, concurrent modification or duplicate resource (e.g. a tax_id already registered).
idempotency_error422 or 409Reusing Idempotency-Key with a different payload (422, idempotency_key_reused) or while its first request is still running (409, idempotency_key_in_use).
rate_limit_error429Exceeded a per-minute or monthly quota, or another budget of the API.
api_error500Unexpected backend error. Retries may help; report to support with request_id.
service_unavailable_error503Public API disabled via kill-switch, or downstream outage (Stripe, mailer).

402 and 403 are not interchangeable. A 402 (payment_required_error) means the operation is available to you and the only thing between you and it is money: register a payment method, fix the declined charge, or contract the plan or add-on it bills to. A 403 means access itself is denied — either the key lacks the scope or the company's plan doesn't include the feature (both authorization_error) — and no payment retry changes that. The pair addon_required (402, the current plan grants zero capacity for the operation) and addon_not_active (403, there is no active eligible plan) is the one worth reading twice. The additional tax ID gate draws the same line: additional_nif_plan_required (402, there is no current plan to bill) and additional_nif_not_in_plan (403, the plan is current but does not include additional tax IDs).

Business rule violations (invalid status transition, an action not allowed in the current document state) respond 422 with type: invalid_request_error and code: invalid_status_transition — not 409. 409 conflict_error is reserved for duplicate creation and concurrency conflicts; an Idempotency-Key still in use is 409 idempotency_error.

Code catalog

The anchor of each H3 heading matches exactly the value of the envelope code field. The doc_url returned by the API resolves to the specific section. The list below covers the codes you will encounter in practice; the live OpenAPI reference documents the exact codes per endpoint.

For the complete reference of every error code grouped by bounded context, with its HTTP status and type, see All error codes.

invalid_request_error

parameter_invalid

A request parameter is missing or invalid. param points to the offending field (e.g. client_id, lines[0].quantity).

Reference page: parameter_invalid.

parameter_invalid_format

A value's format is incorrect for its semantics (regex, length, encoding, a malformed UUID, an out-of-format date).

Reference page: parameter_invalid_format.

parameter_invalid_range

A numeric or date value is outside the allowed range (e.g. limit outside 1..100).

Reference page: parameter_invalid_range.

parameter_invalid_cursor

The starting_after / ending_before cursor is not a valid resource id. See Pagination.

Reference page: parameter_invalid_cursor.

parameter_unknown

The request carries a parameter the endpoint does not accept: an undocumented body field on a strict endpoint, a filter outside its allowlist, a sort field that is not sortable, the offset-style page — v1 paginates by cursor — or a header retired from v1. param names what is not accepted and subcode tells the causes apart: the retired X-Active-Profile header returns subcode: retired_active_profile_header. See Acting on behalf.

Reference page: parameter_unknown.

invalid_param_format

Format constraint failed on a typed field — e.g. the Idempotency-Key header or the Factuarea-Version header is malformed.

Reference page: invalid_param_format.

invalid_param_value

The value does not meet a constraint (enum, format, semantic rule).

Reference page: invalid_param_value.

invalid_period

The requested reporting period is invalid (e.g. a quarter/year that does not exist).

Reference page: invalid_period.

invalid_status_transition

The requested transition is forbidden by the document state machine (e.g. sending an invoice that is not in a sendable state). Business rule violations like this are 422, not 409.

Reference page: invalid_status_transition.

invoice_already_paid

mark-paid on an already-paid invoice.

Reference page: invoice_already_paid.

quote_already_accepted

Action that conflicts with a quote already accepted.

Reference page: quote_already_accepted.

business_rule_violation

A domain invariant blocked the operation. The subcode pinpoints the rule and param the offending field. Used by the payment ledger (Recording payments):

  • payment_exceeds_pending_amount (param: "amount") — the payment amount is greater than the invoice's pending balance. Applies to both POST /v1/companies/{company}/invoices/{id}/payments and POST /v1/companies/{company}/purchase-invoices/{id}/payments.
  • invalid_payment_date (param: "paid_on") — the payment date is outside the allowed issue_date … today window (expenses).
  • purchase_invoice_not_payable (param: "status") — the expense is cancelled and no longer accepts payments.
  • max_webhook_endpoints_reached — the plan-derived cap of company webhook endpoints was reached. Delete an unused endpoint or move to a plan with a higher cap. Account endpoints have their own cap, which answers account_webhook_endpoint_limit_reached.
  • webhook_secret_recently_rotated — the signing secret of a company or account webhook endpoint was rotated less than five minutes ago. It is a 422, not a rate limit: wait, deploy the new secret in your receiver and rotate again.
  • additional_nif_limit_reached — adding the tax ID would leave the account above its cap of additional tax IDs. It is checked before any charge. See Additional tax IDs.
  • offer_package_loss_unconfirmed (param: "accept_offer_package_loss") — reducing the contracted seats, or cancelling the add-on, would break an applied offer package and you did not confirm the loss. details carries product, package_id, name, effective_at and lines, each line with concept, from_price, to_price and interval. Resend the request with accept_offer_package_loss: true to accept the loss. REST and MCP return the same response. See Reducing seats and Additional tax IDs.
  • contracted_seats_selection_required (param: "deactivate") — reducing the contracted seats below the occupied ones requires choosing in deactivate exactly occupied - quantity active entities of the account. On cancel it is returned only for a list that does not match every occupied seat, because without a list all of them are understood. details carries product, requested, occupied, required, selected and invalid. REST and MCP return the same response. See Reducing seats and Additional tax IDs.
  • contracted_seats_exhausted — the web application only: it refuses to create or reactivate an employee or a tax ID when no free contracted seat is left. The v1 API and MCP never return it: they buy the missing seats instead (see Automatic purchase). details carries product, contracted, occupied, free, required and missing.

contracted_seats_below_minimum is retired: the contract draft foresaw it, but it was never emitted. Reducing seats below the minimum of an offer no longer fails; it asks you to confirm the loss of the package (offer_package_loss_unconfirmed).

{
  "error": {
    "type": "invalid_request_error",
    "code": "business_rule_violation",
    "subcode": "payment_exceeds_pending_amount",
    "message": "El importe del pago (1.500,00 €) supera el importe pendiente de la factura (710,00 €).",
    "param": "amount",
    "doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation",
    "request_id": "req_..."
  }
}

Reference page: business_rule_violation.

unsupported_format

The requested export/report format is not supported.

Reference page: unsupported_format.

insufficient_data_for_report

Not enough data to generate the requested tax report.

Reference page: insufficient_data_for_report.

signature_payload_too_large

The delivery-note signature image exceeds the maximum size.

Reference page: signature_payload_too_large.

managed_account_plan_not_self_serve

You asked for the enterprise plan on a managed account. A managed account is created or changed on emprendedor or empresario; enterprise is only assigned by Factuarea under contract. Choose one of the two, or contact Factuarea to agree the contract. See Choose the plan.

Reference page: managed_account_plan_not_self_serve.

managed_account_provider_in_trial

Your account is in its trial period and asked for a live managed account, which is charged from the first day. During the trial you can only create test managed accounts, with a fact_test_ key. Create it with a test key, or start a paid subscription and repeat the provisioning. See Your account in trial or with a payment problem.

Reference page: managed_account_provider_in_trial.

managed_accounts_limit_reached

Your account already has as many live managed accounts under active management as its cap allows (max_managed_accounts). Test managed accounts do not count. End the management of one you no longer run, or contact Factuarea to raise the cap. See The cap.

Reference page: managed_accounts_limit_reached.

managed_account_not_claimable

The managed account is a test account, and a test account is never claimed: it has no claim voucher to issue or reissue. Reissue the voucher only for live managed accounts. See Test managed accounts.

Reference page: managed_account_not_claimable.

access_level_raise_forbidden

You tried to raise the access level on a managed account that its holder has already claimed. Before the claim the provider can raise or lower its level; since the claim only the holder can raise it, and the provider can only lower it. Ask the holder to raise it from their app, or repeat the operation with a level equal to or lower than the current one. See Your access level.

Reference page: access_level_raise_forbidden.

enterprise_contract_transition_required

The managed account has an enterprise contract, and ending its management would move its payment to the holder before Factuarea has set the plan it continues on. A subscription is never created without a price, so nothing changed and the management stays in force. Contact Factuarea so we set the plan, then repeat the operation. See Enterprise by contract.

Reference page: enterprise_contract_transition_required.

import_file_invalid

One of the import files cannot be read with the template format: it is not a readable CSV, its header is not within its first 20 lines, it lacks a required column or it has a broken line. param says which file fails (accounts_file or customers_file) and the message names the column or the line. Nothing was written. An invalid value in one row is not this error: it shows up as that row's error in the preview. See The accounts file.

Reference page: import_file_invalid.

import_file_too_large

One of the import files exceeds 5 MB. It answers 413, and the size is checked before any row is read, so nothing was processed or written. param says which file fails. Shrink the file, or split it into several imports under 5 MB and apply them one after another. See Limits.

Reference page: import_file_too_large.

import_row_limit_exceeded

One of the import files carries more rows than a single import accepts: 100 in accounts_file and 1,000 in customers_file. Rows are counted before being read, so nothing was written. param says which file fails. Split the file and apply the parts one after another: the import is re-runnable, so a row that was already imported creates nothing new. See Limits.

Reference page: import_row_limit_exceeded.

account_webhook_endpoint_limit_reached

The account reached the number of account webhook endpoints its tier allows in that environment. The cap is counted per account and per environment (live or test), includes disabled endpoints and is independent of the cap on company endpoints: only deleting an endpoint frees a slot. Delete one you no longer use or move to a higher tier; one endpoint can subscribe to several event types. A tier that allows none answers 402 addon_required instead. See The cap per tier.

Reference page: account_webhook_endpoint_limit_reached.

authentication_error

missing_api_key

No authentication header present (Authorization: Bearer or X-API-Key).

Reference page: missing_api_key.

invalid_api_key

The key does not exist or the secret doesn't match the stored hash.

Reference page: invalid_api_key.

api_key_revoked

The key was revoked, and a revoked key never authenticates again. Create a new one in the dashboard.

Reference page: api_key_revoked.

api_key_expired

The secret is right, but the key passed its expiry date (expires_at). Issue a new key; if you set expiry dates, plan the rotation before the date so the integration never goes dark.

Reference page: api_key_expired.

ip_not_allowed

The key restricts the addresses it accepts, and the request came from one outside that list. Add the outbound address of your server to the allowlist of the key, or use a key without an IP restriction if your address changes.

Reference page: ip_not_allowed.

too_many_auth_failures

Repeated authentication failures from your client have been throttled. Back off and verify your credentials.

Reference page: too_many_auth_failures.

payment_required_error

A 402 means a commercial prerequisite is missing. The call may buy seats of additional tax IDs or employees, or charge a managed account, there may be no plan to bill it to, or the current plan may grant zero capacity for it. It is not retryable as-is: resolve the payment or change plan first, then repeat the same request. The other two answers of the additional tax ID gate are 403: additional_nif_not_in_plan and billing_managed_by_provider.

Version note. Three of these codes shipped before this category existed and were served as invalid_request_error. They carry payment_required_error from Factuarea-Version: 2026-09-01 onwards: payment_method_required, employee_seat_payment_method_required and employee_seat_charge_failed. Requests on an earlier version keep the old type unchanged. The additional tax ID codes (additional_nif_plan_required and additional_nif_charge_failed) and addon_required are newer and carry payment_required_error on every version. error.code, error.subcode and the 402 status are identical on every version — branch on code and you never have to think about this.

payment_method_required

Buying additional tax ID seats charges them immediately: the PATCH /v1/accounts/{account}/additional-nif-seats endpoint, and also POST /v1/accounts/{account}/companies, the activation endpoints and accepting a tax ID transfer into an account that already has a holder when no free seat is left and the call buys the missing ones (Automatic purchase). The account operates in live mode with a paid subscription and no payment method on file. Provisioning a paid managed account raises it too, when the provider account that pays for it has no payment method. The response carries details.payment_setup_url: open it, register a card and repeat the call. See Additional tax IDs and Managed accounts: when you are charged.

{
  "error": {
    "type": "payment_required_error",
    "code": "payment_method_required",
    "message": "La cuenta no tiene un método de pago configurado para cobrar el NIF adicional: configúralo y vuelve a intentarlo.",
    "details": { "payment_setup_url": "https://billing.stripe.com/p/session/live_YWNjdF8xS2ZHM0RLb0h4RXBGV3lY" },
    "request_id": "req_01HKQS5NPAYMENTMETHODREQ01",
    "doc_url": "https://docs.factuarea.com/guides/errors#payment_method_required"
  }
}

Reference page: payment_method_required.

additional_nif_charge_failed

The immediate charge for the additional tax ID seats was declined — card refused, authentication required, or the payment provider unreachable. When the charge fails the seats are not bought, the tax ID is not added or activated, and no charge remains. Fix the payment method in the billing portal and repeat the same call; check with your bank if the card keeps being declined.

Reference page: additional_nif_charge_failed.

additional_nif_plan_required

The account has no current plan, so there is no subscription to bill the additional tax ID to. Nothing was charged and no change was written. Subscribe to empresario or enterprise, or resume the cancelled plan, and repeat the same call.

Reference page: additional_nif_plan_required.

employee_seat_payment_method_required

Buying employee seats charges them immediately: the seat purchase endpoints, and the v1 and MCP employee creation or reactivation when no free contracted seat is left and the call buys the missing ones (Automatic purchase). The company operates in live mode with no payment method on file. Same remedy as payment_method_required, and the response also carries details.payment_setup_url. Occupying a free contracted seat never charges.

Reference page: employee_seat_payment_method_required.

employee_seat_charge_failed

The pro-rated charge for the employee seats was declined. The seats are not bought, so the employee is not created or activated when the call was buying the missing seats. Fix the payment method and retry; check with your bank if the card keeps being declined. See Payment errors.

Reference page: employee_seat_charge_failed.

employee_seats_not_in_plan

The account has a current plan, but it does not include time tracking (the control_horario module: empresario, integral and enterprise), so employee seats cannot be bought. It is a plan limit and not a missing payment, which is why it is a 403 and not a 402. Nothing was charged and no change was written; lowering or keeping the contracted seats stays allowed. Move the account to Professional or higher and repeat the same call. See Payment errors.

Reference page: employee_seats_not_in_plan.

addon_required

The tier derived from the current plan allows zero webhook endpoints, so POST /v1/companies/{company}/webhook-endpoints returns 402 with the legacy subcode: webhooks_addon_required. Move to a plan that includes webhooks and retry. This is not a missing scope and there is no separate Developer API add-on to purchase. POST /v1/accounts/{account}/webhooks answers the same 402 with subcode: account_webhooks_addon_required when the tier of the account's payer allows no account endpoints at all; once it allows some, reaching the cap is a 422: account_webhook_endpoint_limit_reached.

Reference page: addon_required.

authorization_error

insufficient_scope

The key lacks the scope required by the endpoint. See the catalog at Authentication › Scopes. The super-scope * does not cover the privileged accounts:read and accounts:write: a key that holds only * answers insufficient_scope on a provisioning operation. See the super-scope.

Reference page: insufficient_scope.

privileged_scope_required

The operation requires a privileged scope (accounts:read or accounts:write) and it is not met: either the account of the key has not been granted the provider privilege, or the person who issued the key is no longer an owner or an admin of the account. Both causes answer alike, on purpose. Ask Factuarea for the privilege on your account, or issue a new key with a person who owns or administers the account and repeat the operation. See Privileged scopes.

Reference page: privileged_scope_required.

live_credential_required

A test key (fact_test_) tried to change a live account. It only happens on the operations that change the account itself (team, invitations, ownership, tax ID transfers, the tax ID portfolio, keys and seats): a test key cannot alter real data. Repeat the operation with a live key (fact_live_) of that account; reads stay available with the key you already use. See Test managed accounts.

Reference page: live_credential_required.

company_read_only

The effective access level on the tax ID is view, so the key can read it but not write to it, and nothing was created or changed. The level is the lower of the one stored on the key and the one the person who issued it holds today: a view grant in the account, or a view granted access from another account. Limit the integration to reads on that tax ID, or ask whoever manages that access to raise it to operate and issue the key again. See The level ceiling per tax ID.

Reference page: company_read_only.

forbidden_action

The action is refused even though the scope is right. error.message says which case applies:

  • The key reaches the tax ID through a relation between accounts —a granted access or a managed account— and the operation belongs to the account that owns the tax ID: request logs, events, company webhook endpoints, account billing, automation usage and writes of connected payment credentials. A relation never opens them, whatever the level, reads included. Use a credential of the owning account.
  • The tax ID belongs to a managed account whose holder already pays for it: its provider can read it and edit its data, but the holder decides when to activate, deactivate or archive it.
  • The person who issued the key is not an owner or an admin of the account, or the key does not reach every tax ID of the account, on an operation of the account itself.

See Resolution and errors.

Reference page: forbidden_action.

company_inactive

The company named by the {company} path segment is within the credential's scope, but its tax ID is not active: its status is inactive (deactivated) or archived, so it cannot be operated. An inactive tax ID keeps all its data and can be reactivated; an archived one is terminal. suspension_reason says why the system suspended a tax ID, and it is null when the account deactivated it. Reactivate the tax ID, or point the path at another company in your scope. See The state of a tax ID and Resolution and errors.

Reference page: company_inactive.

additional_nif_not_in_plan

The account has a current plan, but it does not include additional tax IDs: only empresario and enterprise (trials included) allow them. It is a plan limit and not a missing payment, which is why it is a 403 and not a 402. Nothing was charged and no change was written. Move the account to one of those plans and repeat the same call. See The gate and its four codes.

Reference page: additional_nif_not_in_plan.

billing_managed_by_provider

Another account pays the billing of this account (it is a managed account that its provider pays for), so it cannot buy an additional tax ID or an employee seat on its own. Nothing was charged and no change was written. Ask the account that pays to add, activate or accept the tax ID. See What the client cannot buy while you pay.

Reference page: billing_managed_by_provider.

max_api_keys_exceeded

The company has reached its maximum number of active API keys. It answers 422, like the other caps of your plan.

Reference page: max_api_keys_exceeded.

feature_not_available_in_plan

The current plan does not include the required module (e.g. recurring_invoices).

Reference page: feature_not_available_in_plan.

addon_not_active

The company has no active Factuarea plan that includes public API access — for example, the 10-day trial expired or the subscription lapsed outside its grace period. Subscribe to or renew a plan to keep using the API.

Reference page: addon_not_active.

not_found_error

resource_not_found

The resource doesn't exist or doesn't belong to your company.

Reference page: resource_not_found.

profile_not_found

The {company} path segment names a company that does not exist or is not within the credential's NIF scope. Both cases answer the same so that the API never reveals companies of other accounts. Use the id of one of the companies listed by GET /v1/accounts/{account}/companies; the identifier travels in the path, not in any header.

Reference page: profile_not_found.

tax_report_not_found

The requested tax report does not exist.

Reference page: tax_report_not_found.

conflict_error

resource_already_exists

Attempt to create a duplicate (e.g. a tax_id already registered). The subcode (e.g. tax_id_already_exists) pinpoints the duplicate key.

Reference page: resource_already_exists.

resource_conflict

The operation conflicts with the current state of the resource (e.g. a concurrent modification).

Reference page: resource_conflict.

provisioning_account_claimed

The holder has already claimed this managed account and runs it on their own: it can no longer be claimed, have its claim voucher reissued or be provisioned again with the same external_ref. Check its state instead of retrying; for another managed account, use a different external_ref. See The claim.

Reference page: provisioning_account_claimed.

managed_account_external_ref_in_other_environment

The external_ref is already used by a managed account of the other environment (live or test). It is unique across both, and the answer does not name that account because this environment does not list it. Nothing was written. Use a different external_ref, for example with a different prefix in each environment. See Idempotency.

Reference page: managed_account_external_ref_in_other_environment.

import_in_progress

A bulk import of your account is already in progress. Imports of the same account are applied one at a time, so that no account, charge or tax ID is created twice; this one was not applied and nothing was written. Wait for the running import to finish and repeat this one with the same file: it is re-runnable, and the rows the other import created come back as already existing, with no new accounts or charges. See One import at a time.

Reference page: import_in_progress.

idempotency_error

idempotency_key_reused

Same Idempotency-Key, different request body. Use a new key. See Idempotency.

Reference page: idempotency_key_reused.

rate_limit_error

rate_limit_exceeded

You exceeded the per-minute or monthly quota of your tier. The Retry-After header indicates the seconds to wait. See Rate limits.

Reference page: rate_limit_exceeded.

api_error

internal_error

Unexpected error. Already captured on our side, but share request_id with support.

Reference page: internal_error.

service_unavailable_error

service_unavailable

The public API is temporarily unavailable — globally disabled via kill-switch, in a maintenance window, or a downstream dependency (database, mailer, Stripe) is unhealthy. Retry after a short back-off.

Reference page: service_unavailable.

Typed errors with the official SDK

The TypeScript and PHP SDKs map this envelope to a typed exception hierarchy, so you branch on a class (and read code, type, param, request_id) instead of parsing JSON. Your API key is never included in any exception.

import {
  FactuareaError,
  ValidationError,
  RateLimitError,
} from "@factuarea/sdk";

try {
  await factuarea.invoices.create(body);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(error.fields);     // { client_id: ["obligatorio"], … }
  } else if (error instanceof RateLimitError) {
    console.error(error.retryAfter); // seconds to wait
  } else if (error instanceof FactuareaError) {
    console.error(error.code, error.type, error.requestId);
  }
}

The hierarchy also exports AuthenticationError, NotFoundError, ConflictError, ServerError and ConnectionError.

use Factuarea\Sdk\Models\Errors\ErrorThrowable;

try {
    $factuarea->invoices->publicApiV1InvoicesCreate($body);
} catch (ErrorThrowable $e) {
    $error = $e->container->error;
    echo $error->type->value;  // e.g. "invalid_request_error"
    echo $error->code;         // e.g. "parameter_invalid"
    echo $error->param;        // e.g. "client_id"
    echo $error->requestId;    // quote this to support
}

See SDKs › Handling errors for the full hierarchy. The retry policy below is applied automatically by both SDKs.

request_id and support

Every response includes a request_id. Attach it to any ticket or request to support@factuarea.com:

Subject: 422 on POST /v1/companies/{company}/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV

With the request_id we correlate logs, metrics and traces to investigate quickly.

Retry strategy

  • 4xx except 429 → do not retry: the error is in the request. Fix and resend.
  • 429 → respect the Retry-After header. Implement exponential back-off with jitter.
  • 5xx → exponential back-off (2^n * 100ms) with jitter, max 5 attempts.

Stripe publishes a canonical pattern that also applies here: stripe.com/docs/error-handling.

On this page

EnvelopeMulti-field validation errorsDuplication conflictsProblem Details (RFC 9457)Localized messagesError typesCode cataloginvalid_request_errorparameter_invalidparameter_invalid_formatparameter_invalid_rangeparameter_invalid_cursorparameter_unknowninvalid_param_formatinvalid_param_valueinvalid_periodinvalid_status_transitioninvoice_already_paidquote_already_acceptedbusiness_rule_violationunsupported_formatinsufficient_data_for_reportsignature_payload_too_largemanaged_account_plan_not_self_servemanaged_account_provider_in_trialmanaged_accounts_limit_reachedmanaged_account_not_claimableaccess_level_raise_forbiddenenterprise_contract_transition_requiredimport_file_invalidimport_file_too_largeimport_row_limit_exceededaccount_webhook_endpoint_limit_reachedauthentication_errormissing_api_keyinvalid_api_keyapi_key_revokedapi_key_expiredip_not_allowedtoo_many_auth_failurespayment_required_errorpayment_method_requiredadditional_nif_charge_failedadditional_nif_plan_requiredemployee_seat_payment_method_requiredemployee_seat_charge_failedemployee_seats_not_in_planaddon_requiredauthorization_errorinsufficient_scopeprivileged_scope_requiredlive_credential_requiredcompany_read_onlyforbidden_actioncompany_inactiveadditional_nif_not_in_planbilling_managed_by_providermax_api_keys_exceededfeature_not_available_in_planaddon_not_activenot_found_errorresource_not_foundprofile_not_foundtax_report_not_foundconflict_errorresource_already_existsresource_conflictprovisioning_account_claimedmanaged_account_external_ref_in_other_environmentimport_in_progressidempotency_erroridempotency_key_reusedrate_limit_errorrate_limit_exceededapi_errorinternal_errorservice_unavailable_errorservice_unavailableTyped errors with the official SDKrequest_id and supportRetry strategy
Need a hand?Contact support