Factuarea API

Gestión de errores

Envoltorio de error normalizado, catálogo de type y code con anclas estables, y estrategia de reintentos.

Toda respuesta de error de la API pública usa un envoltorio JSON consistente. El estado HTTP indica la categoría general; el campo type desambigua y el campo code apunta a la causa específica.

Envoltorio

{
  "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"
  }
}

Campos:

  • type — categoría general del error. Estable y enumerada (lista abajo).
  • code — causa específica. Estable y enumerada.
  • subcode — opcional. Presente cuando el code por sí solo es ambiguo: en los conflictos de duplicación 409 señala la clave duplicada exacta (p. ej. subcode: "tax_id_already_exists"), y en los errores de pago 402 señala qué gate rechazó la llamada (p. ej. subcode: "webhooks_addon_required"). Como el code, es estable e invariante entre idiomas y versiones de la API.
  • message — texto para personas en español. No se garantiza estable entre versiones; útil para logging y visualización.
  • param — opcional, presente en errores de validación. Apunta al primer campo problemático. En errores de validación de varios campos el conjunto completo está en errors[] (ver abajo).
  • errors[] — opcional, presente en errores de validación 422. Lista todos los campos fallidos (ver Errores de validación de varios campos).
  • details — opcional. Lleva existing_resource_id en los conflictos de duplicación 409 (ver Conflictos de duplicación) y payment_setup_url en los errores 402 que necesitan un método de pago configurado (ver payment_required_error).
  • doc_url — opcional. Enlace a esta guía con ancla al code específico (#{code}).
  • request_id — identificador único de la petición (req_<ULID>). Inclúyelo siempre cuando contactes con soporte. También se devuelve en el header de respuesta X-Request-Id.

El objeto error siempre lleva type, code y message; el resto de campos están presentes cuando es relevante.

Errores de validación de varios campos

Un error de validación 422 reporta todos los campos fallidos, no solo el primero. Los param/message planos siguen reflejando el primer campo (por retrocompatibilidad), y errors[] lleva un item por campo fallido — así corriges todos en una sola petición en vez de una petición por campo.

{
  "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"
  }
}

Cada item de errors[] lleva:

  • param — el nombre del campo fallido.
  • code — un código estable y machine-readable derivado de la regla de validación fallida (p. ej. parameter_missing, parameter_invalid_format, parameter_invalid_enum, parameter_invalid_integer).
  • message — descripción legible del error del campo.
  • expected_format — opcional. Presente solo en errores de formato; el patrón esperado (p. ej. YYYY-MM-DD, uuid, email, url).
  • allowed_values — opcional. Presente solo en errores de enum; la lista de valores legales.

errors[] es puramente aditivo — las integraciones que solo leen param, code y message siguen funcionando sin cambios.

Conflictos de duplicación

Un conflicto de duplicación 409 (code: resource_already_exists con un subcode de tax_id_already_exists, external_id_already_exists o sku_already_exists) devuelve el id del recurso preexistente en details.existing_resource_id. Resuélvelo con un solo GET en vez de un find_by_* adicional.

{
  "error": {
    "type": "conflict_error",
    "code": "resource_already_exists",
    "subcode": "tax_id_already_exists",
    "message": "Ya existe un cliente con ese NIF.",
    "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" },
    "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM",
    "doc_url": "https://docs.factuarea.com/guides/errors#resource_already_exists"
  }
}

Un GET /v1/clients/0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40 devuelve el recurso existente (200). Lo mismo aplica en PUT cuando un external_id ya pertenece a otro recurso de la empresa.

Detalles del problema — Problem Details (RFC 9457)

Envía Accept: application/problem+json para recibir el mismo error como un documento Problem Details de RFC 9457 con Content-Type: application/problem+json. Con Accept: application/json, Accept: */* o sin header Accept obtienes el envoltorio plano de arriba.

{
  "type": "https://docs.factuarea.com/errors/resource_already_exists",
  "title": "Resource already exists",
  "status": 409,
  "detail": "Ya existe un cliente con ese NIF.",
  "instance": "/v1/clients",
  "code": "resource_already_exists",
  "subcode": "tax_id_already_exists",
  "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" },
  "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM"
}
  • type — la página de documentación de ese code concreto, por ejemplo https://docs.factuarea.com/errors/resource_already_exists. El code es el único segmento variable, así que puedes construir y comparar la URI por tu cuenta. Antes era una sola URI compartida por todos los problemas: si comparas contra ella, compara mejor contra code, que no se mueve nunca.
  • title — un resumen humano breve del tipo de problema.
  • status — el código de estado HTTP.
  • detail — el mensaje legible.
  • instance — el path del recurso afectado.

La variante problem+json no descarta ningún dato extendido: code, subcode, param, errors[], details, doc_url y request_id se conservan como miembros de extensión RFC 9457.

Mensajes localizados

El message (y el detail de problem+json) se localiza vía el header Accept-Language para los códigos del catálogo estable. Los idiomas soportados son es, en y ca, con fallback a es cuando el header está ausente o pide un idioma no soportado. El code y el subcode son invariantes entre idiomas — ramifica siempre por code, nunca por message.

Accept-Language: en        → mensaje en inglés
Accept-Language: ca-ES     → mensaje en catalán
(ausente / Accept-Language: de) → mensaje en español (fallback)

Los mensajes dinámicos emitidos por excepciones de dominio quedan en español; solo se localizan los mensajes del catálogo estable.

Tipos de error

typeHTTPDescripción
invalid_request_error400 o 422Payload malformado, parámetros faltantes/inválidos o fallo de validación de negocio.
authentication_error401La API key falta, es inválida, está revocada, ha expirado o la IP no está en la lista de acceso.
payment_required_error402La operación cobra dinero y el pago no ha podido completarse: no hay método de pago configurado, el cargo se denegó, o hace falta una suscripción o un add-on que no está contratado.
authorization_error403La key es válida pero el scope no cubre el endpoint.
permission_error403El plan de la empresa no da acceso a la funcionalidad.
not_found_error404El recurso solicitado no existe o no pertenece a la empresa de la key.
conflict_error409Conflicto de creación, lock de idempotencia o recurso duplicado (p. ej. un tax_id ya registrado).
idempotency_error409Reutilización de Idempotency-Key con un payload distinto.
rate_limit_error429Superada la cuota por minuto o mensual, o demasiados fallos de autenticación.
api_error500Error inesperado del backend. Los reintentos pueden ayudar; reporta a soporte con el request_id.
service_unavailable_error503API pública deshabilitada vía kill-switch, o caída de una dependencia (Stripe, mailer).

402 y 403 no son intercambiables. Un 402 (payment_required_error) significa que la operación está a tu alcance y lo único que se interpone es el dinero: configura un método de pago, resuelve el cargo denegado, o contrata el plan o el add-on al que se factura. Un 403 significa que el acceso en sí está denegado —o la key no tiene el scope (authorization_error), o el plan de la empresa no incluye la funcionalidad (permission_error)— y ningún reintento de pago lo cambia. La pareja addon_required (402, el add-on no está contratado) y addon_not_active (403, ninguna key llega a una funcionalidad no contratada) es la que conviene leer dos veces.

Las violaciones de reglas de negocio (transición de estado inválida, una acción no permitida en el estado actual del documento) responden 422 con type: invalid_request_error y code: invalid_status_transitionno 409. 409 conflict_error se reserva para creación duplicada, conflictos de idempotencia y locks de concurrencia.

Catálogo de codes

El ancla de cada encabezado H3 coincide exactamente con el valor del campo code del envoltorio. El doc_url que devuelve la API resuelve a la sección específica. La lista de abajo cubre los codes que encontrarás en la práctica; la referencia OpenAPI en vivo documenta los codes exactos por endpoint.

Para la referencia completa de cada error code agrupado por bounded context, con su estado HTTP y type, consulta Todos los error codes.

invalid_request_error

parameter_invalid

Un parámetro de la petición falta o es inválido. param apunta al campo problemático (p. ej. client_id, lines[0].quantity).

parameter_invalid_format

El formato de un valor es incorrecto para su semántica (regex, longitud, codificación, un UUID malformado, una fecha fuera de formato).

parameter_invalid_range

Un valor numérico o de fecha está fuera del rango permitido (p. ej. limit fuera de 1..100).

parameter_invalid_cursor

El cursor starting_after / ending_before no es un id de recurso válido. Consulta Paginación.

parameter_unknown

El body contiene un campo no documentado (en endpoints estrictos).

invalid_param_format

Falló una restricción de formato en un campo tipado — p. ej. el header Idempotency-Key o el header Factuarea-Version está malformado.

invalid_param_value

El valor no cumple una restricción (enum, formato, regla semántica).

invalid_period

El periodo de reporte solicitado es inválido (p. ej. un trimestre/año que no existe).

invalid_status_transition

La transición solicitada está prohibida por la máquina de estados del documento (p. ej. enviar una factura que no está en un estado enviable). Las violaciones de reglas de negocio como esta son 422, no 409.

invoice_already_paid

mark-paid sobre una factura ya pagada.

quote_already_accepted

Acción que entra en conflicto con un presupuesto ya aceptado.

business_rule_violation

Una invariante de dominio bloqueó la operación. El subcode identifica la regla y param el campo infractor. Lo usa el ledger de pagos (Registrar pagos):

  • payment_exceeds_pending_amount (param: "amount") — el importe del pago es mayor que el saldo pendiente de la factura. Aplica tanto a POST /v1/invoices/{id}/payments como a POST /v1/purchase_invoices/{id}/payments.
  • invalid_payment_date (param: "paid_on") — la fecha de pago cae fuera de la ventana permitida fecha_emisión … hoy (facturas de compra).
  • purchase_invoice_not_payable (param: "status") — la factura de compra está cancelada y ya no admite pagos.
{
  "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

El formato de exportación/reporte solicitado no está soportado.

insufficient_data_for_report

No hay datos suficientes para generar el reporte de impuestos solicitado.

signature_payload_too_large

La imagen de firma del albarán supera el tamaño máximo.

authentication_error

missing_api_key

No hay header de autenticación presente (Authorization: Bearer o X-API-Key).

invalid_api_key

La key no existe o el secreto no coincide con el hash almacenado.

api_key_revoked

La key fue revocada. Crea una nueva en el dashboard.

too_many_auth_failures

Se han limitado fallos de autenticación repetidos desde tu cliente. Espera (back off) y verifica tus credenciales.

payment_required_error

Todo 402 viene de una operación que cobra algo en el momento en que la llamas: un asiento de empresa gestionada, un asiento de empleado o un add-on. Ninguno se puede reintentar tal cual: resuelve antes el pago y repite la misma petición.

Nota de versión. Cinco de estos códigos se publicaron antes de que existiera esta categoría y se servían como invalid_request_error. Llevan payment_required_error desde Factuarea-Version: 2026-09-01 en adelante: payment_method_required, seat_charge_failed, gestoria_plan_required, employee_seat_payment_method_required y employee_seat_charge_failed. Las peticiones en una versión anterior conservan el type de siempre. error.code, error.subcode y el estado 402 son idénticos en todas las versiones: ramifica por code y no tendrás que pensar en esto.

payment_method_required

POST /v1/companies y los endpoints de activación cobran un asiento de inmediato, y la gestoría opera en modo real sin método de pago configurado. La respuesta lleva details.payment_setup_url: ábrelo, registra una tarjeta y repite la llamada.

{
  "error": {
    "type": "payment_required_error",
    "code": "payment_method_required",
    "message": "La gestoría no tiene un método de pago configurado: configúralo para añadir la empresa.",
    "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"
  }
}

seat_charge_failed

El cargo prorrateado del asiento de la empresa gestionada fue denegado —tarjeta rechazada, autenticación requerida, o el proveedor de pago inaccesible—. La empresa no se crea si el asiento no se cobra. Arregla el método de pago en el portal de facturación y reintenta.

gestoria_plan_required

La gestoría no tiene una suscripción de pago activa, así que no hay suscripción sobre la que cobrar el asiento. Contrata un plan (o reanuda el que canceló) antes de añadir empresas gestionadas.

employee_seat_payment_method_required

Dar de alta o reactivar un empleado cobra un asiento de inmediato, y la empresa opera en modo real sin método de pago configurado. El remedio es el mismo que en payment_method_required, y la respuesta también lleva details.payment_setup_url.

employee_seat_charge_failed

El cargo prorrateado del asiento de empleado fue denegado. El empleado no se activa si el asiento no se cobra. Arregla el método de pago y reintenta; consulta con tu banco si la tarjeta se sigue denegando.

addon_required

La operación pertenece a un add-on que la empresa no ha contratado: por ejemplo, POST /v1/webhook_endpoints requiere el add-on Developer API, cuyo nivel gratuito permite cero endpoints (subcode: webhooks_addon_required). Contrata el add-on y repite la llamada. A diferencia de addon_not_active (403), aquí lo que falta es la contratación, no el scope.

authorization_error

insufficient_scope

La key no tiene el scope que requiere el endpoint. Consulta el catálogo en Autenticación › Scopes.

permission_error

feature_not_available_in_plan

El plan actual no incluye el módulo requerido (p. ej. recurring_invoices).

addon_not_active

La empresa no tiene un plan de Factuarea activo que incluya acceso a la API pública — por ejemplo, el trial de 10 días caducó o la suscripción venció fuera de su periodo de gracia. Contrata o renueva un plan para seguir usando la API.

not_found_error

resource_not_found

El recurso no existe o no pertenece a tu empresa.

tax_report_not_found

El reporte de impuestos solicitado no existe.

conflict_error

resource_already_exists

Intento de crear un duplicado (p. ej. un tax_id ya registrado). El subcode (p. ej. tax_id_already_exists) señala la clave duplicada.

resource_conflict

La operación entra en conflicto con el estado actual del recurso (p. ej. una modificación concurrente).

max_api_keys_exceeded

La empresa ha alcanzado su número máximo de API keys activas.

idempotency_error

idempotency_key_reused

Mismo Idempotency-Key, body de petición distinto. Usa una key nueva. Consulta Idempotencia.

rate_limit_error

rate_limit_exceeded

Superaste la cuota por minuto o mensual de tu tier. El header Retry-After indica los segundos a esperar. Consulta Límites de peticiones.

api_error

internal_error

Error inesperado. Ya está capturado por nuestra parte, pero comparte el request_id con soporte.

service_unavailable_error

service_unavailable

La API pública no está disponible temporalmente — deshabilitada globalmente vía kill-switch, en una ventana de mantenimiento, o una dependencia (base de datos, mailer, Stripe) no está sana. Reintenta tras un back-off corto.

Errores tipados con el SDK oficial

Los SDKs de TypeScript y PHP mapean este envoltorio a una jerarquía de excepciones tipada, así ramificas según una clase (y lees code, type, param, request_id) en vez de parsear JSON. Tu API key nunca se incluye en ninguna excepción.

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);
  }
}

La jerarquía también exporta AuthenticationError, NotFoundError, ConflictError, ServerError y 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
}

Consulta SDKs › Gestión de errores para ver la jerarquía completa. La política de reintentos de abajo la aplican automáticamente ambos SDKs.

request_id y soporte

Toda respuesta incluye un request_id. Adjúntalo a cualquier ticket o petición a support@factuarea.com:

Subject: 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV

Con el request_id correlacionamos logs, métricas y trazas para investigar rápido.

Estrategia de reintentos

  • 4xx excepto 429no reintentes: el error está en la petición. Corrígelo y reenvía.
  • 429 → respeta el header Retry-After. Implementa back-off exponencial con jitter.
  • 5xx → back-off exponencial (2^n * 100ms) con jitter, máximo 5 intentos.

Stripe publica un patrón canónico que también aplica aquí: stripe.com/docs/error-handling.

En esta página