Skip to content

Error Handling

Cord uses conventional HTTP codes and a predictable error structure to indicate success or failure.

HTTP Status Codes

The Cord API always returns standard HTTP status codes. As a general rule:

  • 2xx indicates success.
  • 4xx indicates an error originating from the provided information (e.g., missing parameters, invalid token).
  • 5xx indicates an error on Cord’s servers (these are very rare).

Code Summary

Code Description
200 OK Everything worked as expected.
400 Bad Request The request was unacceptable. Usually means invalid_request (missing fields) or invalid_json; also invalid_cursor or invalid_api_version.
401 Unauthorized Your API Key is missing, incorrect, or was revoked.
402 Payment Required The operation requires a higher plan (subscription_required), exceeds a limit on your current plan (plan_limit_reached, e.g. number of clients or products), or the key exceeds the number of keys your plan includes (subscription_key_limit).
403 Forbidden You have insufficient permissions (e.g., trying to write with a read-only key, or a Publishable Key attempting an action outside its scope).
404 Not Found The resource (quote, client, invoice) does not exist.
409 Conflict An invalid state transition for the resource (invalid_state) — for example, approving a quote that is still a draft, or trying to delete test data (/api/test-mode/reset) without being in a Sandbox organization. Also returned while another request with the same Idempotency-Key is still running (idempotency_in_progress), or when a stamped invoice needs a credit note instead of being voided (credit_note_required).
413 Payload Too Large The body exceeds 1 MB (payload_too_large); on POST /api/v1/elements/ai-draft, the file exceeds 4 MB, and on POST /api/v1/setup/plans, the file exceeds 3 MB.
415 Unsupported Media Type The body wasn’t sent as application/json (unsupported_media_type).
422 Unprocessable Entity You reused an Idempotency-Key with a different request (idempotency_key_reused), or the AI found no products in the text (no_items).
429 Too Many Requests You exceeded a requests-per-minute limit (rate_limited, see Rate limits), your plan’s monthly API quota (api_quota_exceeded), or its AI quota (ai_quota_exceeded). Respect the Retry-After header.
502 Bad Gateway An external provider rejected or didn’t complete the operation: the tax provider when issuing (provider_error) or sending an email (send_failed).
503 Service Unavailable A dependency the operation needs didn’t respond — for example, an exchange rate could not be obtained (fx_unavailable) or the tax catalog (tax_catalog_unavailable), or your subscription couldn’t be verified (subscription_verification_unavailable). Cord never invents a rate: if it can’t prove one, the operation fails closed. Retry later.

Error Object Structure

When a 4xx or 5xx error occurs, Cord always responds with a flat (not nested) JSON object with two fields: error (a human-readable message you can show or log) and code (a stable string for your handling logic). Some errors add extra fields depending on the case (see examples below).

Error Response Example:

{
  "error": "The company name is required",
  "code": "invalid_request"
}

Example with extra fields (plan limit, 402):

{
  "error": "Your plan allows 50 clients. Free up space or upgrade your plan to continue.",
  "code": "plan_limit_reached",
  "resource": "clients",
  "limit": 50,
  "plan": "starter"
}

Don’t rely on the nested error.code / error.message shape to parse the response: error is always the message string, and code is a sibling field at the same level, not a property of error.

Every /api/v1 response carries the Cord-Request-Id header (req_…). On an error, the body repeats it as request_id and adds doc_url, a link to that code’s explanation on this page. Include the request_id when you contact support: it lets us find your exact request.

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

Common Error Codes (code)

  • invalid_json: Your POST request body is not valid JSON, or Content-Type: application/json wasn’t sent.

  • invalid_request: A business validation failed (missing field, discount out of range, a quote without a valid client). The error field explains the exact reason.

  • invalid_fiscal_data (400): The tax details in cliente.fiscal fail the country’s validation. The response includes issues, a list of { field, code }. See Tax form.

  • plan_invalido (400): A quote’s plan_pagos doesn’t go through: it doesn’t add up to the total, a payment is below the minimum charge, a date has passed or it has interest without cliente_empresa: true, among others. The response carries issues, a list of { codigo, hito, valores } with each reason. See Milestone payment plan.

  • aprobacion_solo_cliente (409): You tried to approve with action: "approve" a quote whose payment plan carries interest. Your client approves that one from their link, accepting the plan. Nothing was applied.

  • plan_cambio (409) / aceptacion_requerida (400) / cobro_por_hitos (409) / plan_pagos_sin_aprobar / plan_pagos_descuadrado (409): The API v1 doesn’t return them; the public pages your client opens (the quote link or the invoice link) do, and they show the message. plan_cambio: the plan your client saw is no longer the one for that day, because interest counts from approval; the page reloads with today’s amounts. aceptacion_requerida: your client still has to accept the plan with interest. cobro_por_hitos: the invoice of a quote with a milestone plan isn’t paid from its link; the response carries url, the quote link where each milestone is paid. plan_pagos_sin_aprobar / plan_pagos_descuadrado: the plan isn’t approved yet, or what’s saved doesn’t add up to the quote total; nothing is charged. See Milestone payment plan.

  • missing_key / invalid_key: The Authorization: Bearer header is missing, or the key doesn’t exist / was revoked.

  • token_expired: The credential of an authorized connection (Zapier, Make) expired. The app renews it on its own with its refresh token.

  • insufficient_scope: Your key doesn’t have the required scope (a read-only key tried to write, or a Publishable Key tried a route outside its allowed list).

  • insufficient_permissions (403): A restricted key (rk_) has no permission for that route’s resource, or was used with MCP. The error field names the resource.

  • ip_not_allowed (403): The key has allowed IPs and the request came from another one. The error field includes the IP Cord saw.

  • key_expired (401): The key expired, or it was rolled and its grace period ended. Use the new key or create another.

  • not_found: The requested resource doesn’t exist in your organization.

  • subscription_required: The capability requires a higher plan. The response includes plan (your current one) and plan_required.

  • plan_limit_reached: You hit a hard limit on your plan (clients, products, or active quotes). The response includes resource, limit, and plan.

  • integration_limit_reached (403): your integrations already created the 100 webhook subscriptions allowed per organization. Delete the ones you no longer use.

  • rate_limited / api_quota_exceeded: Requests-per-minute or monthly API quota limit. Retry while respecting Retry-After.

  • fx_unavailable / tax_catalog_unavailable: A third-party data point (exchange rate or tax catalog) could not be obtained with certainty. The operation did not complete — don’t assume it did.

  • subscription_key_limit (402): Your organization has more active keys than its plan includes (for example, after a downgrade) and this key falls outside the allowance; the oldest ones keep working. Revoke keys you no longer use or upgrade. The allowance counts active secret, restricted, and publishable keys, excluding connections authorized through OAuth: 2 on Free, 5 on Starter, 20 on Professional, 50 on Scale, and 200 on Developer.

  • subscription_verification_unavailable (503): Cord couldn’t verify your subscription or record the call’s usage. The operation didn’t run; retry.

  • invalid_api_version (400): The Cord-Version header asks for a version that doesn’t exist. The message lists the available ones. See API Versions.

  • invalid_cursor (400): The cursor isn’t valid. Use the previous response’s next_cursor exactly as it came.

  • payload_too_large (413): The JSON body exceeds 1 MB, or the file sent to POST /api/v1/elements/ai-draft exceeds 4 MB.

  • unsupported_media_type (415): The body isn’t sent as application/json (or as multipart/form-data on POST /api/v1/elements/ai-draft).

  • test_mode_only (403): The /api/v1/test_helpers simulators only work with an sk_test_ key and against the test environment.

  • credit_note_required (409): The invoice is already stamped and can’t be voided directly. Issue a credit note with action: "credit_note".

  • provider_error (502): The tax provider rejected or didn’t complete issuing. The error field explains why.

  • send_failed (502): The invoice email couldn’t be sent. Retry sending.

  • ai_quota_exceeded (429) / no_items (422) / ai_unavailable (502 or 503) / missing_text / text_too_long (400): Errors from the AI routes (POST /api/v1/cotizaciones/ia and POST /api/v1/elements/ai-draft): your plan’s AI allowance ran out, the text had no products, AI isn’t available or couldn’t process the request, or the text is missing or over the maximum. On ai-draft these codes arrive as an error event inside the stream.

  • invalid_state: The resource is not in a status that allows the operation, or it changed while we were processing it. Nothing was applied.

  • invalid_url: A webhook URL is not https:// or points to an internal host.

  • invalid_idempotency_key / idempotency_key_reused / idempotency_in_progress / idempotency_unavailable: See the next section.

  • secret_key_in_browser (403): A Secret Key arrived from a browser. It is already exposed in the page source: revoke it in the API keys tab of the Cord Workbench and call the API only from your server.

  • missing_origin / unauthorized_origin / invalid_origin (403): A Publishable Key arrived without Origin, or from a domain that is not in your organization’s allowed domains (Settings › Embeddable quote builder).

  • origin_allowlist_required (403): A live Publishable Key only works once allowed domains are set in Settings › Embeddable quote builder. pk_test_ keys work from any origin.

Rate limits

Cord applies these limits per one-minute window unless stated otherwise. When you exceed one it responds 429 with code: "rate_limited", generally with the Retry-After header in seconds. Those marked strict don’t let requests through when Cord can’t count them: on /api/v1/cotizaciones/{id} and /api/v1/elements/ai-draft they then respond 503 with error: "access_temporarily_unavailable".

Limit Scope Applies to
120 per minute Per source IP Every request to /api/v1 and to the MCP server, with any key, before the key is validated.
600 per minute Per secret key (sk_, rk_, or OAuth connection) Every request to /api/v1 and to the MCP server.
120 per minute Per publishable key (pk_) The four routes a pk_ accepts.
120 per minute (strict) Per organization POST and DELETE on /api/v1/cotizaciones/{id} and the MCP tools that change a quote’s state.
20 per minute Per key POST /api/v1/webhooks.
20 per minute Per organization POST /api/v1/cotizaciones/ia.
6 per minute per IP, 60 per minute and 500 per day per organization (strict) IP and organization POST /api/v1/elements/ai-draft.
6 per day (strict) Per organization POST /api/v1/setup/plans and proponer_configuracion in MCP.

Because the per-IP limit is lower than a secret key’s, a server that sends everything from a single IP hits 120 per minute first. Apart from these limits, live keys consume your plan’s monthly API quota (api_quota_exceeded).

Safe retries with Idempotency-Key

If a request that creates or changes something (POST, PATCH, DELETE) is cut off by the network or a timeout, you do not know whether Cord applied it. Send the Idempotency-Key header with a unique value per operation (for example, a UUID you generate) and retry with the same key and the same body:

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" }'
  • Retry with the same key and body: Cord returns the original response without running the operation again or counting it against your quota. The repeated response includes the Idempotent-Replayed: true header.
  • Same key with a different body, method, or route: 422 idempotency_key_reused. Use a new key for each different operation.
  • The first request has not finished: 409 idempotency_in_progress. Wait and retry.
  • 5xx and 429 errors: are not stored, so you can retry with the same key.
  • The key is scoped to your API key, accepts 1 to 255 visible characters (invalid_idempotency_key otherwise), and expires after 24 hours.
  • If Cord cannot reserve the key, it returns 503 idempotency_unavailable without running the operation.

The header is optional: without it, every request runs as it arrives.