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:
2xxindicates success.4xxindicates an error originating from the provided information (e.g., missing parameters, invalid token).5xxindicates 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: YourPOSTrequest body is not valid JSON, orContent-Type: application/jsonwasn’t sent. -
invalid_request: A business validation failed (missing field, discount out of range, a quote without a valid client). Theerrorfield explains the exact reason. -
invalid_fiscal_data(400): The tax details incliente.fiscalfail the country’s validation. The response includesissues, a list of{ field, code }. See Tax form. -
plan_invalido(400): A quote’splan_pagosdoesn’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 withoutcliente_empresa: true, among others. The response carriesissues, a list of{ codigo, hito, valores }with each reason. See Milestone payment plan. -
aprobacion_solo_cliente(409): You tried to approve withaction: "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 carriesurl, 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: TheAuthorization: Bearerheader 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. Theerrorfield names the resource. -
ip_not_allowed(403): The key has allowed IPs and the request came from another one. Theerrorfield 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 includesplan(your current one) andplan_required. -
plan_limit_reached: You hit a hard limit on your plan (clients, products, or active quotes). The response includesresource,limit, andplan. -
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 respectingRetry-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): TheCord-Versionheader asks for a version that doesn’t exist. The message lists the available ones. See API Versions. -
invalid_cursor(400): Thecursorisn’t valid. Use the previous response’snext_cursorexactly as it came. -
payload_too_large(413): The JSON body exceeds 1 MB, or the file sent toPOST /api/v1/elements/ai-draftexceeds 4 MB. -
unsupported_media_type(415): The body isn’t sent asapplication/json(or asmultipart/form-dataonPOST /api/v1/elements/ai-draft). -
test_mode_only(403): The/api/v1/test_helperssimulators only work with ansk_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 withaction: "credit_note". -
provider_error(502): The tax provider rejected or didn’t complete issuing. Theerrorfield 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/iaandPOST /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. Onai-draftthese codes arrive as anerrorevent 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 nothttps://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 withoutOrigin, 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: trueheader. - 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. 5xxand429errors: 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_keyotherwise), and expires after 24 hours. - If Cord cannot reserve the key, it returns
503 idempotency_unavailablewithout running the operation.
The header is optional: without it, every request runs as it arrives.