Errors & rate limits
JSON-RPC error shapes mapped from the v1 contract, the full code table, and per-token / per-plan throttling with Retry-After.
The MCP server speaks strict JSON-RPC 2.0. Failures come back as a
error object, never as an HTTP error body the way the REST API does — but the
semantics are identical: the same business-rule violation that returns 422
over REST returns the equivalent error here — a JSON-RPC error, or an isError result
when it is raised inside a tool — with the v1 code and http_status preserved.
This page covers the MCP-specific JSON-RPC mapping and the MCP throttling
buckets. For the canonical REST contract — the error envelope by code, and the
per-tier quotas — see Errors and
Rate limits.
Errors raised while a tool runs
A domain error raised while a tool executes — a business rule (422), a missing resource (404), a conflict (409) — does not come back as a JSON-RPC error. The protocol reserves that for protocol failures, so the tool answers with a normal result flagged isError: true, which the model can read and correct:
{
"jsonrpc": "2.0",
"id": "<request id>",
"result": {
"isError": true,
"content": [{ "type": "text", "text": "La factura ya emitida no puede modificarse." }],
"_meta": {
"com.factuarea/error": {
"http_status": 422,
"code": "invoice_cannot_be_modified",
"hint": "La factura ya emitida no puede modificarse.",
"param": "status"
}
}
}
}content[0].textis the domain message (the samehintthe REST API returns), also in production._meta["com.factuarea/error"]carries the contract of the REST error:http_status,code,hintand, when they apply,param,subcodeanddetails(for exampleexisting_resource_idof a duplicate). Branch oncode, as you do over REST.- Only a
5xxis reported as a fault of ours; a rejection is normal use.
The guards that run before the tool — authentication, scopes and plan, throttling, fiscal guardrails, idempotency and the retired tool names — still answer with a JSON-RPC error, as the rest of this page describes.
Error shape
{
"jsonrpc": "2.0",
"id": "<request id>",
"error": {
"code": -32008,
"message": "invoice_cannot_be_modified",
"data": {
"http_status": 422,
"code": "invoice_cannot_be_modified",
"hint": "La factura ya emitida no puede modificarse.",
"param": "status"
}
}
}error.code— the JSON-RPC numeric code (business mappings use-32099..-32000; protocol errors include-32602and internal errors use-32603).error.message— a stable string identifier (e.g.insufficient_scope,invoice_cannot_be_modified).error.data.code— the same canonical v1codethe REST API returns, so you can branch on one value across both surfaces.error.data.http_status— the HTTP status the equivalent REST call would return (422 / 404 / 409 / …), for clients that prefer to reason in HTTP terms.error.data.hint— a human-readable message (in Spanish, matching the app's locale). Other fields (param,subcode,required_scope, …) appear when relevant.
Code table
| JSON-RPC code | message / data.code | HTTP equiv. | Meaning |
|---|---|---|---|
-32001 | invalid_token | 401 | Missing, malformed or unknown credential; or the user is no longer a member of the company. |
-32002 | client_revoked | 403 | The OAuth client was revoked. |
-32003 | invalid_token_type | 403 | Wrong credential type for this surface. |
-32004 | plan_limit_exceeded / plan_upgrade_required | 402 | The annual document limit of the plan was reached. For plan_limit_exceeded, data carries resource, current, limit and period_end (full ISO 8601 timestamp with offset), plus http_status, code and hint. plan_upgrade_required keeps its existing meaning: the action needs a higher plan. |
-32005 | insufficient_scope / module_not_in_plan / feature_flag_disabled | 403 | The credential lacks the required scope, the module isn't in the plan, or a feature flag is off. data carries required_scope / module / flag. |
-32006 | rate_limit_exceeded | 429 | A throttle bucket was exceeded. data carries retry_after and bucket; the response also sets the Retry-After header. |
-32007 | addon_not_active | 403 | The company has no active Factuarea plan that includes public API access (e.g. an expired trial or lapsed subscription outside its grace period). |
-32008 | (v1 code) | 422 / 404 / 409 / … | A guard that runs before the tool (permissions, fiscal guardrail, idempotency, retired tool names, company selection) rejecting with a business rule, missing resource or conflict. Errors raised inside a tool arrive as a result instead (see above). message and data.code are the canonical v1 error code; data.http_status tells you the category. |
-32603 | internal_error | 500 | Unexpected server error. |
-32005 and -32008 each cover several sub-causes. Always branch on
data.code (the string), not only on the numeric code, when you need to tell
them apart — e.g. insufficient_scope vs module_not_in_plan both surface as
-32005.
Insufficient scope
When a tool needs a scope the credential doesn't hold:
{
"jsonrpc": "2.0",
"id": "req-42",
"error": {
"code": -32005,
"message": "insufficient_scope",
"data": {
"http_status": 403,
"code": "insufficient_scope",
"required_scope": "invoices:write",
"provided_scopes": ["invoices:read", "contacts:read"],
"hint": "La credencial no tiene el scope requerido para esta operación."
}
}
}Tools your credential can't reach are also hidden from tools/list, so a
well-behaved agent won't normally attempt them — this error is the safety net.
Rate limits
Requests are throttled across three independent buckets. Exceeding any one
returns -32006 with a Retry-After header (seconds).
Per-token, by tool category
Each credential has separate per-minute counters per tool category, so heavy destructive use can't starve your reads:
| Category | Default limit | Example tools |
|---|---|---|
read / write | 60 / min | search_invoices, create_invoice |
send | 20 / min | send_invoice, send_quote |
generate | 30 / min | bulk_invoices_pdf_link, generate_tax_report_303 |
destructive | 10 / min | delete_invoice, bulk_archive_contacts |
The bucket is resolved from the tool's category. The counter is incremented before the tool runs, so rejected calls (bad scope, validation error) still consume quota — this is deliberate anti-abuse, matching the standard OAuth/REST pattern.
Per-plan, hourly
A company-wide hourly cap by plan slug. The Enterprise plan bypasses this bucket entirely.
Per-OAuth-client, global
OAuth apps additionally share a global per-client bucket of 1000 / min, so a single misbehaving app can't overwhelm the server across all its users.
Rate-limit headers
Successful responses carry the remaining budget so you can back off proactively:
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Token / X-RateLimit-Remaining-Token | The per-token (per-category) bucket. |
X-RateLimit-Limit-Hour / X-RateLimit-Remaining-Hour | The per-plan hourly bucket (absent on Enterprise). |
Retry-After | On a 429, seconds to wait before retrying. |
Auth endpoint limits
The OAuth endpoints have their own limits, independent of the MCP buckets:
| Endpoint | Limit |
|---|---|
POST /api/oauth/register | 10 / hour per IP |
POST /api/oauth/token | 60 / min per (client, IP) |
Always honor Retry-After. Retrying before it elapses keeps the bucket full
and only delays your recovery. Combine it with idempotency on writes so a
delayed retry never duplicates a document.
Protocol errors and fiscal guardrails
A protocol lookup can return -32602 when a tool name cannot be resolved; inspect discovery and credential scope before retrying. mcp_guardrail_violation is a 422 business rejection carrying rule_id, guardrail_uri and a subcode. Read the referenced fiscal resource, correct the arguments and follow the tool’s idempotency policy.