Manejo de Errores
Cord utiliza códigos HTTP convencionales y una estructura de error predecible para indicar éxito o fallo.
Códigos de Estado HTTP
La API de Cord siempre devuelve códigos de estado HTTP estándar. Como regla general:
2xxindican éxito.4xxindican un error originado por la información proveída (ej. parámetros faltantes, token inválido).5xxindican un error en los servidores de Cord (son muy raros).
Resumen de Códigos
| Código | Descripción |
|---|---|
| 200 OK | Todo funcionó como se esperaba. |
| 400 Bad Request | La petición era inaceptable. Generalmente significa invalid_request (faltan campos) o invalid_json; también invalid_cursor o invalid_api_version. |
| 401 Unauthorized | Falta tu Clave de API, es incorrecta o fue revocada. |
| 402 Payment Required | La operación requiere un plan superior (subscription_required), excede un límite de tu plan actual (plan_limit_reached, ej. número de clientes o productos) o la llave excede el número de llaves que incluye tu plan (subscription_key_limit). |
| 403 Forbidden | Tienes permisos insuficientes (ej. intentaste escribir con una clave que solo tiene permisos de lectura, o una Publishable Key intentó una operación fuera de su alcance). |
| 404 Not Found | El recurso (cotización, cliente, factura) no existe. |
| 409 Conflict | Un cambio de estado inválido para el recurso (invalid_state) — por ejemplo, aprobar una cotización que sigue en borrador, o borrar datos de prueba (/api/test-mode/reset) sin estar en una organización tipo Sandbox. También cuando otra petición con la misma Idempotency-Key sigue en proceso (idempotency_in_progress), o cuando una factura timbrada necesita una nota de crédito en lugar de anularse (credit_note_required), o cuando otra petición está emitiendo la misma factura (issuance_in_progress). idempotency_in_progress trae Retry-After: 2 y Cord-Should-Retry: true. |
| 413 Payload Too Large | El cuerpo supera 1 MB (payload_too_large); en POST /api/v1/elements/ai-draft, el archivo supera 4 MB, y en POST /api/v1/setup/plans, el archivo supera 3 MB. |
| 415 Unsupported Media Type | El cuerpo no se envió como application/json (unsupported_media_type). |
| 422 Unprocessable Entity | Reutilizaste una Idempotency-Key con una petición distinta (idempotency_key_reused), o la IA no encontró productos en el texto (no_items), o el proveedor fiscal o la autoridad rechazaron la emisión de una factura (fiscal_rejected). |
| 429 Too Many Requests | Superaste un límite de peticiones por minuto (rate_limited, ver Límites de peticiones), la cuota mensual de API de tu plan (api_quota_exceeded) o la de IA (ai_quota_exceeded). Respeta el header Retry-After. rate_limited trae Cord-Should-Retry: true; una cuota agotada (api_quota_exceeded, ai_quota_exceeded) y la ventana de 3 envíos por factura cada 10 minutos traen Cord-Should-Retry: false, porque no se resuelven al reintentar. |
| 502 Bad Gateway | Un proveedor externo no respondió o no completó la operación: el proveedor fiscal al emitir (provider_error) o el envío de un correo (send_failed). |
| 503 Service Unavailable | Un servicio del que depende la operación no respondió — por ejemplo, no se pudo obtener un tipo de cambio (fx_unavailable) o el catálogo de impuestos (tax_catalog_unavailable), o no se pudo verificar tu suscripción (subscription_verification_unavailable). Cord nunca inventa una tasa: si no la puede demostrar, la operación falla cerrada. Reintenta más tarde. |
Estructura del Objeto Error
Cuando ocurre un error 4xx o 5xx, Cord siempre responde con un objeto JSON plano (no anidado) con dos campos: error (mensaje humano, para mostrarlo o registrarlo) y code (string estable para tu lógica de manejo). Algunos errores agregan campos adicionales según el caso (ver ejemplos abajo).
Ejemplo de Respuesta de Error:
{
"error": "El nombre de la empresa es obligatorio",
"code": "invalid_request"
}
Ejemplo con campos adicionales (límite de plan, 402):
{
"error": "Tu plan permite 50 clientes. Libera espacio o sube de plan para continuar.",
"code": "plan_limit_reached",
"resource": "clients",
"limit": 50,
"plan": "starter"
}
No confíes en la forma anidada error.code / error.message para parsear la respuesta: error es siempre el string del mensaje y code es un campo hermano al mismo nivel, no una propiedad de error.
Toda respuesta de /api/v1 lleva el header Cord-Request-Id (req_…). En un error, el cuerpo lo repite como request_id y agrega doc_url, el enlace a la explicación de ese código en esta página. Incluye el request_id cuando escribas a soporte: con él encontramos tu petición exacta.
{
"error": "API key inválida o revocada.",
"code": "invalid_key",
"request_id": "req_3f9c2a1b7d4e6f8a0b1c2d3e",
"doc_url": "https://docs.cordhq.app/docs/desarrolladores/esenciales/errores#invalid_key"
}
Códigos de Error Comunes (code)
Los códigos que puede responder cada operación, con su status, están en su ficha de la Referencia de la API.
-
invalid_json: El cuerpo de tu peticiónPOSTno es JSON válido, o no se envióContent-Type: application/json. -
invalid_request: Fallan las validaciones de negocio (falta un campo, un descuento fuera de rango, una cotización sin cliente válido). El campoerrorexplica la razón exacta. -
invalid_fiscal_data(400): Los datos fiscales decliente.fiscalno pasan la validación del país. La respuesta traeissues, una lista de{ field, code }. Ver Formulario fiscal. -
plan_invalido(400): Elplan_pagosde una cotización no procede: no suma el total, un pago queda bajo el mínimo de cobro, una fecha ya pasó o trae intereses sincliente_empresa: true, entre otros. La respuesta traeissues, una lista de{ codigo, hito, valores }con cada motivo. Ver Plan de pagos por hitos. -
aprobacion_solo_cliente(409): Intentaste aprobar conaction: "approve"una cotización cuyo plan de pagos lleva intereses. Esa la aprueba tu cliente desde su link, aceptando el plan. No se aplicó nada. -
plan_cambio(409) /aceptacion_requerida(400) /cobro_por_hitos(409) /plan_pagos_sin_aprobar/plan_pagos_descuadrado(409): No los responde la API v1, sino las páginas públicas que abre tu cliente (el link de la cotización o el de la factura), que muestran el mensaje.plan_cambio: el plan que tu cliente vio ya no es el de ese día, porque los intereses se cuentan desde la aprobación; la página se recarga con los importes de hoy.aceptacion_requerida: falta que tu cliente acepte el plan con intereses.cobro_por_hitos: la factura de una cotización con plan por hitos no se paga desde su link; la respuesta traeurl, el link de la cotización donde se paga cada hito.plan_pagos_sin_aprobar/plan_pagos_descuadrado: el plan todavía no está aprobado, o lo guardado no suma el total de la cotización; no se cobra nada. Ver Plan de pagos por hitos. -
missing_key/invalid_key: Falta el headerAuthorization: Bearer, o la llave no existe / fue revocada. -
token_expired: La credencial de una conexión autorizada (Zapier, Make) venció. La app la renueva sola con su refresh token. -
insufficient_scope: Tu llave no tiene el alcance necesario (una llave de solo lectura intentó escribir, o una Publishable Key intentó una ruta fuera de su lista permitida). -
insufficient_permissions(403): Una llave restringida (rk_) no tiene permiso para el recurso de esa ruta, o se usó con MCP. El campoerrornombra el recurso. -
ip_not_allowed(403): La llave tiene IPs permitidas y la petición llegó desde otra. El campoerrorincluye la IP que vio Cord. -
key_expired(401): La llave venció, o se rotó y terminó su periodo de gracia. Usa la llave nueva o genera otra. -
not_found: El recurso solicitado no existe en tu organización. -
subscription_required: La capacidad requiere un plan superior. La respuesta incluyeplan(el actual) yplan_required. -
plan_limit_reached: Alcanzaste un límite duro de tu plan (clientes, productos o cotizaciones activas). La respuesta incluyeresource,limityplan. -
integration_limit_reached(403): tus integraciones ya crearon las 100 suscripciones de webhook permitidas por organización. Borra las que ya no uses. -
rate_limited/api_quota_exceeded: Límite de peticiones por minuto o de cuota mensual de la API. Reintenta respetandoRetry-After. -
fx_unavailable/tax_catalog_unavailable: Un dato de terceros (tipo de cambio o catálogo de impuestos) no se pudo obtener con certeza. La operación no se completó — no asumas que sí ocurrió. -
subscription_key_limit(402): Tu organización tiene más llaves activas de las que incluye su plan (por ejemplo, después de bajar de plan) y esta llave quedó fuera del cupo; las más antiguas siguen funcionando. Revoca llaves que ya no uses o sube de plan. El cupo cuenta llaves secretas, restringidas y publicables activas, sin las conexiones autorizadas por OAuth: 2 en Gratis, 5 en Starter, 20 en Profesional, 50 en Scale y 200 en Developer. -
subscription_verification_unavailable/usage_verification_unavailable(503): Cord no pudo verificar tu suscripción o registrar el uso de la llamada o del envío. La operación no se ejecutó; reintenta. -
invalid_api_version(400): El headerCord-Versionpide una versión que no existe. El mensaje lista las disponibles. Ver Versiones. -
invalid_cursor(400): Elcursorno es válido. Usa tal cual elnext_cursorde la respuesta anterior. -
payload_too_large(413): El cuerpo JSON supera 1 MB, el archivo enviado aPOST /api/v1/elements/ai-draftsupera 4 MB, o el dePOST /api/v1/setup/planssupera 3 MB. -
unsupported_media_type(415): El cuerpo no viene comoapplication/json(o comomultipart/form-dataenPOST /api/v1/elements/ai-draft). -
test_mode_only(403): Los simuladores de/api/v1/test_helperssolo funcionan con una llavesk_test_y sobre el entorno de prueba. -
credit_note_required(409): La factura ya timbrada no se puede anular directamente. Emite una nota de crédito conaction: "credit_note". -
provider_error(502): El proveedor fiscal no respondió o no completó la emisión. Se puede reintentar con la mismaIdempotency-Key; el campoerrorexplica la razón. -
fiscal_rejected(422): El proveedor fiscal, la autoridad o la validación del país rechazaron la emisión. Repetirla da lo mismo: corrige lo que diceerror(datos del receptor, certificado, claves) y vuelve a emitir. -
issuance_in_progress(409): Otra petición está emitiendo la misma factura. Espera unos segundos y consulta la factura antes de reintentar. -
send_failed(502): No se pudo enviar el correo de la factura. Reintenta el envío. -
ai_quota_exceeded(429) /no_items(422) /ai_unavailable(502 o 503) /missing_text/text_too_long(400): Errores de las rutas con IA (POST /api/v1/cotizaciones/ia,POST /api/v1/elements/ai-draftyPOST /api/v1/setup/plans): se agotó el cupo de IA de tu plan, el texto no traía productos, la IA no está disponible o no pudo procesar el pedido, o el texto falta o supera el máximo. Enai-draftestos códigos llegan como eventoerrordentro del stream. -
invalid_state: El recurso no está en un estado que permita la operación, o cambió mientras la procesábamos. No se aplicó nada. -
invalid_url: La URL de un webhook no eshttps://o apunta a un host interno. -
invalid_idempotency_key/idempotency_key_reused/idempotency_in_progress/idempotency_unavailable: Ver la sección siguiente. -
secret_key_in_browser(403): Una Secret Key llegó desde un navegador. Ya quedó expuesta en el código de la página: revócala en la pestaña Llaves del Cord Workbench y llama a la API solo desde tu servidor. -
missing_origin/unauthorized_origin/invalid_origin(403): Una Publishable Key llegó sinOrigin, o desde un dominio que no está en los dominios permitidos de tu organización (Ajustes › Cotizador embebible). -
origin_allowlist_required(403): Una Publishable Key en vivo solo funciona con dominios permitidos configurados en Ajustes › Cotizador embebible. Las llavespk_test_funcionan desde cualquier origen. -
comercio_exterior(409): México, al emitir conaction: "finalize": el CFDI lleva mercancías para un cliente en el extranjero y Cord no genera el complemento de comercio exterior. Si no es una exportación definitiva, vuelve a emitir consin_comercio_exterior: true. No se reservó folio. -
invalid_tax_id: El identificador fiscal no es válido para el país:400al crear una cotización con un cliente nuevo y422enPOST /api/v1/clientesyPATCH /api/v1/clientes/{id}. -
invalid_discount/discount_currency(400): El descuento de documento está fuera de rango, o el descuento de monto o el cupón es de otra divisa que la del documento. -
unknown_tax_rate(400): Eltax_ratede una línea no está en tu catálogo de impuestos. Usa una tasa de tu catálogo. -
unsupported_currency(400):base_currencyofiscal_currencyno es una de las divisas que Cord ofrece. -
approval_pending/approval_required(409): La cotización espera la aprobación interna de gerencia (send), o reenviar su versión nueva la necesita (resend). Se aprueba desde la app; no se envió nada.
Límites de peticiones
Cord aplica estos límites por ventana de un minuto, salvo donde se indica. Al superarlos responde 429 con code: "rate_limited", en general con el header Retry-After en segundos y Cord-Should-Retry: true. Los marcados como estrictos no dejan pasar peticiones cuando Cord no puede contarlas: en /api/v1/cotizaciones/{id} y /api/v1/elements/ai-draft responden entonces 503 con error: "access_temporarily_unavailable".
| Límite | Alcance | Aplica a |
|---|---|---|
| 60 por minuto | Por IP de origen | Peticiones a /api/v1 y al servidor MCP sin llave o con una llave inválida. Al llegar al tope, esa IP espera el resto del minuto, también con una llave válida. |
| 1,200 por minuto | Por IP de origen | Toda petición con llave a /api/v1 y al servidor MCP. |
| 600 por minuto | Por llave secreta (sk_, rk_ o conexión OAuth) |
Toda petición a /api/v1 y al servidor MCP. |
| 120 por minuto | Por llave publicable (pk_) |
Las cuatro rutas que admite una pk_. |
| 120 por minuto (estricto) | Por organización | POST y DELETE en /api/v1/cotizaciones/{id} y las herramientas MCP que cambian el estado de una cotización. |
| 20 por minuto | Por llave | POST /api/v1/webhooks. |
| 20 por minuto | Por organización | POST /api/v1/cotizaciones/ia. |
| 6 por minuto por IP, 60 por minuto y 500 al día por organización (estrictos) | IP y organización | POST /api/v1/elements/ai-draft. |
| 6 al día (estricto) | Por organización | POST /api/v1/setup/plans y proponer_configuracion en MCP. |
| 3 cada 10 minutos (estricto) | Por factura | POST /api/v1/facturas/{id} con action: "send". Su 429 trae Cord-Should-Retry: false. |
Una llave válida se mide por su propio límite: un servidor que manda todo desde una sola IP llega a 600 por minuto por llave secreta. Aparte de estos límites, las llaves en vivo consumen la cuota mensual de API de tu plan (api_quota_exceeded).
Reintentos seguros con Idempotency-Key
Si una petición que crea o cambia algo (POST, PATCH, DELETE) se corta por red o timeout, no sabes si Cord la aplicó. Envía el header Idempotency-Key con un valor único por operación (por ejemplo, un UUID que generes tú) y reintenta con la misma clave y el mismo cuerpo:
curl -X POST "https://cordhq.app/api/v1/clientes" \
-H "Authorization: Bearer sk_live_tU..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d9c2b7e-6f1a-4c3d-9e8b-2a1f0c7d6e5b" \
-d '{ "empresa": "ACME Corp" }'
- Reintento con la misma clave y el mismo cuerpo: Cord devuelve la respuesta original sin volver a ejecutar la operación ni contarla en tu cuota. La respuesta repetida trae el header
Idempotent-Replayed: true. - La misma clave con otro cuerpo, método o ruta:
422 idempotency_key_reused. Usa una clave nueva para cada operación distinta. - La primera petición todavía no termina:
409 idempotency_in_progress, conRetry-After: 2yCord-Should-Retry: true. Espera esos segundos y reintenta con la misma clave. - Errores
5xx,429y los que traenCord-Should-Retry: true: no se guardan, así que puedes reintentar con la misma clave. - Header
Cord-Should-Retry: en una respuesta de error,truedice que puedes repetir la misma petición tal cual yfalseque repetirla da el mismo resultado, aunque sea un5xx. Los SDK oficiales lo siguen. - La clave es por llave de API, admite de 1 a 255 caracteres visibles (
invalid_idempotency_keysi no) y caduca a las 24 horas. - Si Cord no puede reservar la clave, responde
503 idempotency_unavailablesin ejecutar la operación.
El header es opcional: sin él, cada petición se ejecuta como llega.
Errores en el SDK de Node
@flouviahq/node convierte cada respuesta de error en un CordError con status, code, message, requestId y docUrl. Desde la versión 1.2.0 lo hace con una subclase según el status, así que puedes atrapar cada caso por su clase sin comparar códigos:
| Clase | Cuándo | Qué trae además |
|---|---|---|
CordAuthenticationError |
401 |
— |
CordPermissionError |
403 |
— |
CordInvalidRequestError |
400, 404 o 422 |
param (el field de la respuesta) e issues |
CordRateLimitError |
429 |
retryAfter, en segundos |
CordIdempotencyError |
409 idempotency_in_progress o 422 idempotency_key_reused |
— |
CordConnectionError |
Sin respuesta (status 0, code network_error) |
cause, el error de red |
CordTimeoutError |
Un intento pasó de timeoutMs |
También es una CordConnectionError |
import { CordInvalidRequestError, CordRateLimitError } from '@flouviahq/node';
try {
await cord.clients.create(input);
} catch (err) {
if (err instanceof CordInvalidRequestError) console.log(err.param, err.message);
else if (err instanceof CordRateLimitError) console.log(err.retryAfter);
else throw err;
}
Los demás errores (402, un 409 de estado, 5xx) llegan como CordError. Todos traen headers con los de la respuesta. El SDK reintenta solo lo que se puede repetir y respeta el header Cord-Should-Retry. Ver SDK de Node.