Errors
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 on some409 conflict_error/resource_already_existsresponses to pinpoint the exact duplicate key — e.g.subcode: "tax_id_already_exists".message— human text in Spanish. Not guaranteed stable across versions; useful for logging and display.param— optional, present in validation errors. Points to the offending field.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.
Error types
| type | HTTP | Description |
|---|---|---|
invalid_request_error | 400 or 422 | Malformed payload, missing/invalid parameters or business validation failure. |
authentication_error | 401 | The API key is missing, invalid, revoked, expired or the IP is not in the allowlist. |
authorization_error | 403 | The key is valid but the scope doesn't cover the endpoint. |
permission_error | 403 | The plan/add-on does not grant access to the feature. |
not_found_error | 404 | The requested resource doesn't exist or doesn't belong to the key's company. |
conflict_error | 409 | Creation conflict, idempotency lock or duplicate resource (e.g. a tax_id already registered). |
idempotency_error | 409 | Reusing Idempotency-Key with a different payload. |
rate_limit_error | 429 | Exceeded the per-minute or monthly quota, or too many auth failures. |
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). |
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,
idempotency conflicts and concurrency locks.
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.
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).
parameter_invalid_format
A value's format is incorrect for its semantics (regex, length, encoding, a malformed UUID, an out-of-format date).
parameter_invalid_range
A numeric or date value is outside the allowed range (e.g. limit
outside 1..100).
parameter_invalid_cursor
The starting_after / ending_before cursor is not a valid resource
id. See Pagination.
parameter_unknown
The body contains an undocumented field (on strict endpoints).
invalid_param_format
Format constraint failed on a typed field — e.g. the Idempotency-Key
header or the Factuarea-Version header is malformed.
invalid_param_value
The value does not meet a constraint (enum, format, semantic rule).
invalid_period
The requested reporting period is invalid (e.g. a quarter/year that does not exist).
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.
invoice_already_paid
mark-paid on an already-paid invoice.
quote_already_accepted
Action that conflicts with a 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/invoices/{id}/paymentsandPOST /v1/purchase_invoices/{id}/payments.invalid_payment_date(param: "paid_on") — the payment date is outside the allowedissue_date … todaywindow (purchase invoices).purchase_invoice_not_payable(param: "status") — the purchase invoice is cancelled and no longer accepts payments.
{
"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_..."
}
}unsupported_format
The requested export/report format is not supported.
insufficient_data_for_report
Not enough data to generate the requested tax report.
signature_payload_too_large
The delivery-note signature image exceeds the maximum size.
authentication_error
missing_api_key
No authentication header present (Authorization: Bearer or
X-API-Key).
invalid_api_key
The key does not exist or the secret doesn't match the stored hash.
api_key_revoked
The key was revoked. Create a new one in the dashboard.
too_many_auth_failures
Repeated authentication failures from your client have been throttled. Back off and verify your credentials.
authorization_error
insufficient_scope
The key lacks the scope required by the endpoint. See the catalog at Authentication › Scopes.
permission_error
feature_not_available_in_plan
The current plan does not include the required module (e.g.
recurring_invoices).
addon_not_active
The developer_api add-on is inactive or outside its grace period.
not_found_error
resource_not_found
The resource doesn't exist or doesn't belong to your company.
tax_report_not_found
The requested tax report does not exist.
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.
resource_conflict
The operation conflicts with the current state of the resource (e.g. a concurrent modification).
max_api_keys_exceeded
The company has reached its maximum number of active API keys.
idempotency_error
idempotency_key_reused
Same Idempotency-Key, different request body. Use a new key. See
Idempotency.
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.
api_error
internal_error
Unexpected error. Already captured on our side, but share request_id
with support.
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.
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/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.