Factuarea API
Core concepts

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 some 409 conflict_error / resource_already_exists responses 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 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.

Error types

typeHTTPDescription
invalid_request_error400 or 422Malformed payload, missing/invalid parameters or business validation failure.
authentication_error401The API key is missing, invalid, revoked, expired or the IP is not in the allowlist.
authorization_error403The key is valid but the scope doesn't cover the endpoint.
permission_error403The plan/add-on does not grant access to the feature.
not_found_error404The requested resource doesn't exist or doesn't belong to the key's company.
conflict_error409Creation conflict, idempotency lock or duplicate resource (e.g. a tax_id already registered).
idempotency_error409Reusing Idempotency-Key with a different payload.
rate_limit_error429Exceeded the per-minute or monthly quota, or too many auth failures.
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).

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_transitionnot 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 both POST /v1/invoices/{id}/payments and POST /v1/purchase_invoices/{id}/payments.
  • invalid_payment_date (param: "paid_on") — the payment date is outside the allowed issue_date … today window (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_01JBVH7K9Y4N3CDQ2EHJB1AGSV

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

Retry strategy

  • 4xx except 429do 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