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 elcodepor sí solo es ambiguo: en los conflictos de duplicación409señala la clave duplicada exacta (p. ej.subcode: "tax_id_already_exists"), y en los errores de pago402señala qué gate rechazó la llamada (p. ej.subcode: "webhooks_addon_required"). Como elcode, 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á enerrors[](ver abajo).errors[]— opcional, presente en errores de validación422. Lista todos los campos fallidos (ver Errores de validación de varios campos).details— opcional. Llevaexisting_resource_iden los conflictos de duplicación409(ver Conflictos de duplicación) ypayment_setup_urlen los errores402que necesitan un método de pago configurado (ver payment_required_error).doc_url— opcional. Enlace a esta guía con ancla alcodeespecí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 respuestaX-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 esecodeconcreto, por ejemplohttps://docs.factuarea.com/errors/resource_already_exists. Elcodees 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 contracode, 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
| type | HTTP | Descripción |
|---|---|---|
invalid_request_error | 400 o 422 | Payload malformado, parámetros faltantes/inválidos o fallo de validación de negocio. |
authentication_error | 401 | La API key falta, es inválida, está revocada, ha expirado o la IP no está en la lista de acceso. |
payment_required_error | 402 | La 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_error | 403 | La key es válida pero el scope no cubre el endpoint. |
permission_error | 403 | El plan de la empresa no da acceso a la funcionalidad. |
not_found_error | 404 | El recurso solicitado no existe o no pertenece a la empresa de la key. |
conflict_error | 409 | Conflicto de creación, lock de idempotencia o recurso duplicado (p. ej. un tax_id ya registrado). |
idempotency_error | 409 | Reutilización de Idempotency-Key con un payload distinto. |
rate_limit_error | 429 | Superada la cuota por minuto o mensual, o demasiados fallos de autenticación. |
api_error | 500 | Error inesperado del backend. Los reintentos pueden ayudar; reporta a soporte con el request_id. |
service_unavailable_error | 503 | API 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_transition —
no 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 aPOST /v1/invoices/{id}/paymentscomo aPOST /v1/purchase_invoices/{id}/payments.invalid_payment_date(param: "paid_on") — la fecha de pago cae fuera de la ventana permitidafecha_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_01JBVH7K9Y4N3CDQ2EHJB1AGSVCon el request_id correlacionamos logs, métricas y trazas para
investigar rápido.
Estrategia de reintentos
4xxexcepto429→ no reintentes: el error está en la petición. Corrígelo y reenvía.429→ respeta el headerRetry-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.