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 wherecodealone is ambiguous: on409duplication conflicts it pinpoints the duplicate key (e.g.subcode: "tax_id_already_exists"), and on402payment errors it pinpoints which gate rejected the call (e.g.subcode: "webhooks_addon_required"). Likecode, 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 inerrors[](see below).errors[]— optional, present on422validation errors. Lists all failed fields (see Multi-field validation errors).details— optional. Carriesexisting_resource_idon409duplication conflicts (see Duplication conflicts) andpayment_setup_urlon the402errors that need a payment method registered (see payment_required_error).doc_url— optional. Link to this guide with anchor to the specificcode(#{code}).request_id— unique request identifier (req_<ULID>). Always include it when contacting support. It is also returned in theX-Request-Idresponse 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 specificcode, e.g.https://docs.factuarea.com/errors/resource_already_exists. Thecodeis 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 oncodeinstead — 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
| type | HTTP | Description |
|---|---|---|
invalid_request_error | 400, 405, 411, 413, 415 or 422 | Malformed or oversized payload, unsupported method or media type, missing/invalid parameters or business validation failure. |
authentication_error | 401 or 429 | The 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_error | 402 | A 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_error | 403 or 422 | The 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_error | 404 | The requested resource doesn't exist or doesn't belong to the key's company. |
conflict_error | 409 | Creation conflict, concurrent modification or duplicate resource (e.g. a tax_id already registered). |
idempotency_error | 422 or 409 | Reusing 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_error | 429 | Exceeded a per-minute or monthly quota, or another budget of the API. |
api_error | 500 | Unexpected backend error. Retries may help; report to support with request_id. |
service_unavailable_error | 503 | Public 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 bothPOST /v1/companies/{company}/invoices/{id}/paymentsandPOST /v1/companies/{company}/purchase-invoices/{id}/payments.invalid_payment_date(param: "paid_on") — the payment date is outside the allowedissue_date … todaywindow (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 answersaccount_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 a422, 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.detailscarriesproduct,package_id,name,effective_atandlines, each line withconcept,from_price,to_priceandinterval. Resend the request withaccept_offer_package_loss: trueto 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 indeactivateexactlyoccupied - quantityactive entities of the account. Oncancelit is returned only for a list that does not match every occupied seat, because without a list all of them are understood.detailscarriesproduct,requested,occupied,required,selectedandinvalid. 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).detailscarriesproduct,contracted,occupied,free,requiredandmissing.
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.
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_01JBVH7K9Y4N3CDQ2EHJB1AGSVWith the request_id we correlate logs, metrics and traces to
investigate quickly.
Retry strategy
4xxexcept429→ do not retry: the error is in the request. Fix and resend.429→ respect theRetry-Afterheader. 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.