Saltar al contenido

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:

  • 2xx indican éxito.
  • 4xx indican un error originado por la información proveída (ej. parámetros faltantes, token inválido).
  • 5xx indican 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ón POST no 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 campo error explica la razón exacta.

  • invalid_fiscal_data (400): Los datos fiscales de cliente.fiscal no pasan la validación del país. La respuesta trae issues, una lista de { field, code }. Ver Formulario fiscal.

  • plan_invalido (400): El plan_pagos de 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 sin cliente_empresa: true, entre otros. La respuesta trae issues, una lista de { codigo, hito, valores } con cada motivo. Ver Plan de pagos por hitos.

  • aprobacion_solo_cliente (409): Intentaste aprobar con action: "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 trae url, 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 header Authorization: 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 campo error nombra el recurso.

  • ip_not_allowed (403): La llave tiene IPs permitidas y la petición llegó desde otra. El campo error incluye 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 incluye plan (el actual) y plan_required.

  • plan_limit_reached: Alcanzaste un límite duro de tu plan (clientes, productos o cotizaciones activas). La respuesta incluye resource, limit y plan.

  • 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 respetando Retry-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 header Cord-Version pide una versión que no existe. El mensaje lista las disponibles. Ver Versiones.

  • invalid_cursor (400): El cursor no es válido. Usa tal cual el next_cursor de la respuesta anterior.

  • payload_too_large (413): El cuerpo JSON supera 1 MB, el archivo enviado a POST /api/v1/elements/ai-draft supera 4 MB, o el de POST /api/v1/setup/plans supera 3 MB.

  • unsupported_media_type (415): El cuerpo no viene como application/json (o como multipart/form-data en POST /api/v1/elements/ai-draft).

  • test_mode_only (403): Los simuladores de /api/v1/test_helpers solo funcionan con una llave sk_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 con action: "credit_note".

  • provider_error (502): El proveedor fiscal no respondió o no completó la emisión. Se puede reintentar con la misma Idempotency-Key; el campo error explica 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 dice error (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-draft y POST /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. En ai-draft estos códigos llegan como evento error dentro 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 es https:// 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ó sin Origin, 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 llaves pk_test_ funcionan desde cualquier origen.

  • comercio_exterior (409): México, al emitir con action: "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 con sin_comercio_exterior: true. No se reservó folio.

  • invalid_tax_id: El identificador fiscal no es válido para el país: 400 al crear una cotización con un cliente nuevo y 422 en POST /api/v1/clientes y PATCH /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): El tax_rate de una línea no está en tu catálogo de impuestos. Usa una tasa de tu catálogo.

  • unsupported_currency (400): base_currency o fiscal_currency no 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, con Retry-After: 2 y Cord-Should-Retry: true. Espera esos segundos y reintenta con la misma clave.
  • Errores 5xx, 429 y los que traen Cord-Should-Retry: true: no se guardan, así que puedes reintentar con la misma clave.
  • Header Cord-Should-Retry: en una respuesta de error, true dice que puedes repetir la misma petición tal cual y false que repetirla da el mismo resultado, aunque sea un 5xx. Los SDK oficiales lo siguen.
  • La clave es por llave de API, admite de 1 a 255 caracteres visibles (invalid_idempotency_key si no) y caduca a las 24 horas.
  • Si Cord no puede reservar la clave, responde 503 idempotency_unavailable sin 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.