Limits, errors and retries.
Rate limits and other limits, idempotency keys, the approval window, and every error code the Agent API and MCP endpoint return, with what to do about each.
On this page
This page is for developers, and for owners who read an error their AI client showed them. It lists the limits, explains how to repeat a call safely, and gives every error code with the action to take.
MCP tools and the Agent API share one error format and one set of codes. Over HTTP the codes arrive with an HTTP status. Over MCP a failed tool call returns isError: true, and the same JSON is in the result text, so the model can read it and correct itself.
The error envelope
{
"error": true,
"code": "VALIDATION_FAILED",
"message": "The request data did not pass validation.",
"retry_strategy": "fix_request",
"details": {
"errors": { "card_id": ["The card_id field is required."] }
}
}
details is optional. Framework errors on agent paths use the same envelope.
Retry strategies
Every error declares what to do next.
retry_strategy |
What to do |
|---|---|
no_retry |
The same request will fail again. Stop, and change the request or the setup. |
fix_request |
Change the input and send it again. Read details and message. |
backoff |
The condition is temporary. Wait, then retry. Honor Retry-After. Retry a change only with the same idempotency key. |
retry_later |
The platform can't do this right now, for example Apple Wallet signing isn't set up. Try later. Don't loop. |
contact_support |
A person must act first: the platform owner turns on a feature, raises a limit or restores an account. |
For backoff, wait the Retry-After seconds when the answer has them. Otherwise wait longer after each try, for example 1, 2, 4, 8 and 16 seconds, then stop.
Limits
| Limit | Value |
|---|---|
| Requests per key | 60 a minute. Each POST to /mcp counts as one request, including initialize and tools/list. |
| Requests per AI connection | 120 a minute for a browser sign-in connection or a key created on the AI connections page. |
| Requests per network address | 3,000 a minute across all keys. The operator can change it with AGENT_IP_RATE_LIMIT. Behind a reverse proxy, set TRUSTED_PROXIES so each caller keeps its own address. |
| Failed sign-ins | 20 a minute per network address and key prefix. Reward Loyalty refuses further attempts before it checks the key. |
| Request body | 256 KiB. |
| Rows per page of a record list | 100, with a total. |
| Tools per list | A connection's tool list stays within 90 tools and 150 KB. Larger sets move into tool families. |
| Email campaign send | Up to 25 emails per call. Send the rest with send_next_campaign_batch. |
| Webhook test and sample sends | 10 a minute per endpoint. |
| Apple Wallet sample checks | 5 a minute. |
| Voiding achievements | 20 a minute for the platform owner. |
| Browser sign-in | 10 client registrations a minute per address. 30 token and revoke requests a minute per address. |
A response to an authenticated request carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After.
Idempotency
An idempotency key lets you repeat a change without doing it twice. Use it whenever a call might time out.
- Over MCP, every tool that changes data takes a required
idempotency_keyargument. - Over HTTP, send an
Idempotency-Keyheader. It is optional. - Format. 1 to 80 characters from
A-Z,a-z,0-9, hyphen and underscore. A UUID works. - A new change gets a new key. Generate a fresh random value for each change.
- A retry reuses the key. Repeat the same request with the same key. Reward Loyalty returns the stored answer, and over HTTP it adds
Idempotent-Replayed: true. Nothing runs twice. - A different request under the same key returns
409 IDEMPOTENCY_CONFLICT. - A request that is still running returns
409 IDEMPOTENCY_IN_PROGRESSwithRetry-After: 1. Retry with the same key. A request that stops without finishing releases its key. Only when its process was killed outright does a later call wait 15 minutes to take the key over. - Only successful answers are stored. That includes a
202that prepared an approval, so a repeat returns the same approval. If a call fails, the key is released and you can try again, with the same key. - An idempotency key belongs to one credential and one operation. The same value sent by another connection starts fresh there. Sent again for another record of the same operation, it returns
409 IDEMPOTENCY_CONFLICT. - A replay is checked again. If the connection has since lost the scope or the plan feature, the replay is refused and nothing is returned.
Reward Loyalty keeps each stored answer for 7 days, encrypted. The scheduled task agent-receipts-retention removes older ones. It runs with the other scheduled tasks and from the Health center, and it needs no queue worker.
Stored-money operations (selling a stored-money pass, spending from it and reversing a spend) and a purchase sent with a source_reference keep their own retry rules. A stored-money operation takes a UUID as its operation key, in the same header or argument. Production checklist covers purchases.
The approval window
A request for approval closes 10 minutes after the AI client prepared it. The owner's decision doesn't extend it. After the owner approves, the AI client must run the change inside the same 10 minutes. See Approvals.
Error codes
Each row gives the code, the HTTP status, the retry strategy and when it happens. A code whose status or strategy differs by case has a row per case, or says so in its row.
Sign-in and access
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
AUTH_MISSING_KEY |
401 | no_retry |
The request carried no credential. Send Authorization: Bearer <key> or X-Agent-Key. At /mcp the answer includes a WWW-Authenticate challenge that points to sign-in discovery. |
AUTH_INVALID_KEY |
401 | no_retry |
The key is malformed, unknown or wrong. Also: a browser sign-in token is expired or invalid, or it was sent anywhere except /mcp. |
AUTH_KEY_REVOKED |
401 | no_retry |
The key or connection was ended. |
AUTH_KEY_EXPIRED |
401 | no_retry |
The key or connection passed its end date. |
AUTH_AMBIGUOUS_KEY |
400 | fix_request |
The request sent both Authorization: Bearer and X-Agent-Key with different values. Send one. |
AUTH_OWNER_INACTIVE |
403 | contact_support |
The account that owns the key is inactive. Every key it owns stops working. |
AUTH_ANONYMOUS_MEMBER |
403 | no_retry |
Anonymous members can't use keys. |
AUTH_WRONG_ROLE |
403 | no_retry |
The credential's role doesn't fit the endpoint, for example a business key on an admin path, or an admin manager's key on a platform path. At /mcp, an owner who can't connect an AI client (an admin manager or a business sub-role) gets contact_support. |
AUTH_INSUFFICIENT_SCOPE |
403 | no_retry |
The credential lacks the scope the call needs. details.required_scope names it when one scope is required. Money records need the explicit read:money scope. |
FEATURE_DISABLED |
403 | contact_support |
A switch, plan feature or permission the call depends on is off. Business keys see it when the Agent API permission is withdrawn or a plan feature is missing. details.permission names the feature when there is one. At /mcp, a business owner without the Agent API gets this answer. |
ORIGIN_NOT_ALLOWED |
403 | no_retry |
The request carried an Origin header, which means it came from a web page. Call the API from a server or a desktop client. |
DEMO_MODE |
403 | no_retry |
A demo installation refuses changes and approvals. Reads still work. |
DEMO_PREVIEW |
403 | no_retry |
A store connection in the demo preview is read-only. |
READ_THROUGH_NOT_ALLOWED |
403 | no_retry |
The X-Agent-Partner header was sent by a credential that can't use it, or on a path that isn't a business read. Only the platform owner's credential can read another business. Approvals aren't readable through another business. |
READ_THROUGH_READ_ONLY |
403 | no_retry |
A change was sent with read-through to another business. Reading through works for reads only. |
FORBIDDEN |
403 | no_retry |
A framework refusal on an agent path with no more specific code. Stored-money operations also use it; see Stored-money passes. |
Request shape
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
VALIDATION_FAILED |
422 | fix_request |
The request didn't pass validation. details.errors maps each field to its messages. Over MCP, a call whose arguments don't match the tool's schema also fails this way. |
BAD_REQUEST |
400 | fix_request |
The request couldn't be read. |
UNSUPPORTED_MEDIA_TYPE |
415 | fix_request |
Send the body as application/json. |
PAYLOAD_TOO_LARGE |
413 | fix_request |
The body is larger than 256 KiB. details.max_bytes gives the limit. Send fewer items per call. |
METHOD_NOT_ALLOWED |
405 | fix_request |
The path doesn't accept this method. GET and DELETE on /mcp get this answer with Allow: POST. |
NOT_FOUND |
404 | no_retry |
The record or path doesn't exist, belongs to someone else, or is hidden from this credential. A wrong approval id or nonce also answers this. |
INVALID_FORMAT |
422 | fix_request |
GET /tools was called with a format it doesn't offer. |
MISSING_CARD_ID |
422 | fix_request |
A member discovery call that needs a card_id didn't send one. |
MISSING_INPUT |
422 | fix_request |
A member discovery call needs a url or an identifier. |
UNRESOLVABLE_INPUT |
404 | fix_request |
The url or identifier didn't resolve to a card. |
CONFLICT |
409 | no_retry |
A framework conflict on an agent path with no more specific code. |
GONE |
410 | no_retry |
A framework answer for a resource that is no longer available. |
Limits and capacity
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
RATE_LIMITED |
429 | backoff |
A limit was reached. details.scope tells which: the key's own limit (no scope), the network address (ip), failed sign-ins (failed_authentication), or a per-action limit such as test webhooks. Wait for Retry-After seconds. |
LIMIT_REACHED |
422 | contact_support |
The business reached a plan limit for the thing it tried to create. details names the resource. For member card codes the strategy is fix_request, because a smaller batch fits. |
RESOURCE_UNAVAILABLE |
404 | no_retry |
The list, form or feature isn't available on this installation or to this account, for example member cards when they are off. |
Repeated calls
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
INVALID_IDEMPOTENCY_KEY |
422 | fix_request |
The idempotency key isn't 1 to 80 characters from A-Z, a-z, 0-9, hyphen and underscore. |
IDEMPOTENCY_CONFLICT |
409 | fix_request |
The key was already used for a different request. Use a new key for a new request. |
IDEMPOTENCY_IN_PROGRESS |
409 | backoff |
A request with this key is still running. Retry with the same key after Retry-After (1 second). |
Approval errors
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
APPROVAL_PENDING |
409 | backoff |
The owner hasn't decided yet. Give them the review link and try again. |
APPROVAL_REJECTED |
409 | no_retry |
The owner rejected the change. It won't run. |
APPROVAL_STALE |
409 | fix_request |
The record changed after the owner reviewed it, or an earlier run failed partway. Check the record, then prepare the change again. |
APPROVAL_USED |
409 | no_retry |
The approval already ran. Read the approval to get its result. |
APPROVAL_EXPIRED |
410 | fix_request |
The 10 minutes passed. Prepare the change again. |
APPROVAL_IN_PROGRESS |
409 | backoff |
The change is running now. Read the approval in a moment. |
APPROVAL_REQUIRED |
409 | no_retry |
An email send to members was called without an approval the owner gave. Nothing was sent. Prepare the send and let the owner approve it. |
APPROVAL_MISMATCH |
409 | no_retry |
The request isn't the change the owner approved, so nothing ran. Prepare the change again. A run that couldn't finish answers INTERNAL_ERROR instead. |
Server and platform
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
INTERNAL_ERROR |
500 | backoff |
An unexpected error. Retry with backoff only if the call is safe to repeat. For a change, check the result first. |
UNAVAILABLE |
503 | backoff |
The service is temporarily unavailable. |
TOOLS_UNAVAILABLE |
503 | backoff |
The tool definitions aren't available yet on this installation. Ask the platform owner. |
SPEC_UNAVAILABLE |
503 | backoff |
The API description could not be read, so Reward Loyalty can't tell whether a change needs approval. It refuses every change until it can read it again, which can happen for a moment while an update rewrites it. Nothing changed, reads still answer, and an approval already given stays open. Try again shortly. If it lasts, ask the platform owner. |
PLATFORM_UNAVAILABLE |
409 | retry_later |
Apple Wallet signing isn't set up on this platform. Try later. Don't loop. |
Changes, corrections and records
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
WRITE_REFUSED |
422 | fix_request |
A business owner's change: the dashboard form refused it, and message says why, for example a plan rule or an unoffered choice. |
WRITE_REFUSED |
409 | no_retry |
A platform owner's change: the console refused it, and message says why. |
SETTING_NOT_AGENT_WRITABLE |
422 | fix_request |
A setting the Agent API can't change: credentials, uploads, language settings, the AI and Agent API switches, or the billing provider. details names it. Nothing was saved. |
CREDENTIALS_EXCLUDED |
422 | fix_request |
A store connection change tried to set credentials, tokens or addresses. Those are set in Reward Loyalty. Nothing changed. |
TRANSLATION_CONFLICT |
409 | fix_request |
A translation edit used an old or missing revision. Read the current texts, then send the batch again with their revisions. |
LANGUAGE_POLICY_CONFLICT |
409 | fix_request |
The languages changed since the revision was read. Read them again and prepare the change with the current revision. |
RETENTION_CHANGED |
409 | fix_request |
The activity log retention changed after the purge was prepared. Nothing was deleted. |
BLANK_CARDS_CHANGED |
409 | fix_request |
The blank cards aren't the ones the owner reviewed. Prepare the deletion again. |
RESOURCE_PROTECTED |
422 | fix_request |
The record is protected. An email campaign still targets the segment (fix_request), or the club can't be deleted (no_retry). |
RECORD_UNDELETABLE |
409 | no_retry |
The record is marked undeletable, so the dashboard never deletes it. |
LEDGER_ENTRY_CHANGED |
409 | fix_request |
A correction named a ledger entry that is no longer the last one. Read the ledger again. If the newest stamp entry was already undone, the strategy is no_retry and the member needs new stamps first. |
NOTHING_TO_UNDO |
409 | no_retry |
There is nothing left to undo or delete, or the redemption was already voided. Nothing changed. |
UNDO_REFUSED |
409 | no_retry |
The platform owner's undo was refused for a reason in message, for example the entry can't be undone. |
ALREADY_VOIDED |
409 | no_retry |
The achievement is already voided. |
Counter: points, stamps and vouchers
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
CARD_INACTIVE |
422 | no_retry |
The card, its club or the business isn't active. |
INSUFFICIENT_POINTS |
422 | no_retry |
The member doesn't have enough points for the reward or deduction. |
INSUFFICIENT_BALANCE |
422 | no_retry |
A member's own redemption asks for more points than any enrolled card holds. |
REWARD_NOT_AVAILABLE |
422 | no_retry |
The reward isn't on offer: it hasn't started, has ended, or is switched off. For a member key it is 403 when no enrolled card offers it. |
REWARD_NOT_LINKED |
422 | fix_request |
The reward isn't offered on this card. |
TRANSACTION_FAILED |
422 | fix_request |
The purchase couldn't be processed. Check the input. |
REDEMPTION_FAILED |
422 | fix_request |
The redemption couldn't be processed. Check the input. |
DEDUCTION_FAILED |
422 | fix_request |
The deduction couldn't be processed. Check the input. |
STAMP_FAILED |
422 | no_retry |
Stamps couldn't be added. message says why. |
REDEEM_FAILED |
422 | no_retry |
The stamp reward couldn't be redeemed. message says why. |
VOUCHER_INVALID |
422 | no_retry |
The voucher code isn't valid. details.error_code says why. |
VOUCHER_REDEEM_FAILED |
422 | no_retry |
The voucher couldn't be redeemed. message says why. |
CODE_POOL_EXHAUSTED |
422 | backoff |
Every points claim code is in use. Wait for some to expire, then retry. |
CODE_ALREADY_USED |
409 | no_retry |
The points claim code was already used, so it is kept for its history. |
PASS_ISSUE_FAILED |
422 | no_retry |
The pass couldn't be sold. message says why. |
PASS_SCAN_FAILED |
422 | no_retry |
The pass couldn't be scanned. message says why. |
PASS_UNDO_FAILED |
422 | no_retry |
The last pass visit couldn't be undone. message says why. |
PURCHASE_CURRENCY_MISMATCH |
422 | no_retry |
The sale currency differs from the card currency. Nothing is recorded. Amounts are never converted. |
SOURCE_COMPLETED_AT_OUT_OF_RANGE |
422 | no_retry |
A new sale can be at most 7 days old and 5 minutes in the future. |
PURCHASE_REFERENCE_IN_FLIGHT |
409 | backoff |
The same sale reference is still being processed. Retry the identical request. |
PURCHASE_REFERENCE_CONFLICT |
409 | no_retry |
The sale reference was recorded with different sale facts. Don't retry. |
PURCHASE_REFERENCE_CONSUMED |
410 | no_retry |
The original purchase was corrected or removed. The sale stays settled. Don't retry. |
PURCHASE_RETRYABLE_FAILURE |
503 | backoff |
A temporary failure interrupted the sale. Retry the identical request with backoff. |
PURCHASE_CLAIM_KEY_UNAVAILABLE |
503 | contact_support |
The application key that protects sale records isn't configured. The operator must restore it from backup. |
Email campaigns, store connections and reports
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
CAMPAIGN_CHANGED |
409 | fix_request |
The draft, its segment or its audience changed after the owner reviewed it. Nothing was sent. |
CAMPAIGN_INCOMPLETE |
422 | fix_request |
The draft is missing something it needs before it can send, such as a subject or a body. |
CAMPAIGN_NOT_DRAFT |
409 | no_retry |
Only a draft can be changed or sent this way. Continue a campaign that is sending with send_next_campaign_batch. |
CAMPAIGN_NOT_SENDING |
409 | no_retry |
The campaign is still a draft, or has nothing left to send. |
CAMPAIGN_SENDING |
409 | backoff |
The campaign is sending, so it can't be deleted yet. |
INTEGRATION_NOT_CONNECTED |
409 | no_retry |
The store is already disconnected. Nothing changed. |
INTEGRATION_NOT_ACTIVE |
409 | no_retry |
The store connection isn't active, so it can't be paused or used. |
INTEGRATION_CANNOT_RESUME |
409 | no_retry |
The store connection can't be resumed from its state. details.state shows it. |
MEMBER_NOT_IN_PROGRAM |
422 | fix_request |
The member isn't in the card's program, so points can't be redeemed for a store coupon. |
INVALID_CARD |
422 | fix_request |
The card can't be used for a store coupon redemption. |
REDEMPTION_REFUSED |
422 | fix_request |
The store coupon redemption was refused. message says why. |
REDEMPTION_INCOMPLETE |
502 | contact_support |
The store coupon redemption didn't finish cleanly. details carries the coupon code and whether points were taken. Check the store. |
MONEY_PASS_USE_MONEY_ANALYTICS |
409 | fix_request |
The pass holds stored money. Use get_money_pass_analytics. |
VISIT_PASS_USE_PASS_ANALYTICS |
409 | fix_request |
The pass counts visits. Use get_pass_analytics. |
Stored-money passes
Stored-money pass operations (sell_prepaid_pass, spend_pass_money, reverse_pass_money_spend, the pass setup and the money analytics) add the codes below. They arrive in the same envelope. A 422 is fix_request and a 5xx is backoff. A 409 is no_retry, except the conflicts you clear by changing the request (fix_request) and the ones a person must clear (contact_support). The rows below say which. A reused operation key with a different body gets 409 IDEMPOTENCY_CONFLICT with fix_request.
| Code | HTTP | Retry | When it happens |
|---|---|---|---|
MONETARY_PASS_FORMAT_REQUIRED |
409 | fix_request |
A pass detail call for a stored-money pass needs pass_format=2. Send it again with that format. |
INVALID_PASS_FORMAT |
422 | fix_request |
The pass_format value isn't one the API offers. |
MONETARY_PASS_REQUIRED |
422 | fix_request |
The call needs a stored-money pass and got a visit pass. |
INVALID_AMOUNT |
422 | fix_request |
An amount isn't a whole number in the currency's minor unit. |
INVALID_MONETARY_FIELDS |
422 | fix_request |
The request carries fields a stored-money operation doesn't accept. |
INVALID_EXTERNAL_REFERENCE |
422 | fix_request |
The external reference must be 1 to 100 characters without control characters. external_source must be pos_receipt, invoice or manual_record. |
INVALID_REASON |
422 | fix_request |
The reason isn't one of the allowed reason codes. |
INVALID_OPERATION_KEY |
422 | fix_request |
The operation key isn't a UUID. |
INVALID_PAYMENT_RECORD |
422 | fix_request |
A sale must record payment_received: true and match the product's price and currency. |
CURRENCY_MISMATCH |
422 | fix_request |
The currency differs from the pass's currency. |
INVALID_BALANCE_TYPE |
422 | fix_request |
The pass balance_type isn't uses or money. |
BALANCE_TYPE_IMMUTABLE |
422 | fix_request |
A pass product can't change between visits and money once created. |
INVALID_MONETARY_TEMPLATE |
422 | fix_request |
The stored-money product settings are invalid. |
INVALID_MONETARY_SETUP |
422 | fix_request |
The stored-money setup isn't complete or valid. |
MONETARY_TEMPLATE_OWNERSHIP_IMMUTABLE |
422 | fix_request |
The product's owner can't change. |
MONETARY_CURRENCY_LOCKED |
422 | fix_request |
The currency can't change after sales exist. |
REPORT_PERIOD_REQUIRED |
422 | fix_request |
A stored-money report needs a period. |
REPORT_CURRENCY_REQUIRED |
422 | fix_request |
A stored-money report needs a currency. |
REPORT_TIMEZONE_REQUIRED |
422 | fix_request |
A stored-money report needs a time zone. |
REPORT_RETENTION_GAP |
409 | no_retry |
The report period reaches back past the retained data. |
UNAUTHORIZED |
401 | no_retry |
The key is invalid, inactive, or no longer matches its owner when the stored-money operation checks it. |
FORBIDDEN |
403 | no_retry |
The key lacks write:passes or write:pass-money, or the pass belongs to another business or club. |
MONETARY_PASS_ACTION_REQUIRED |
409 | fix_request |
A visit-pass action, such as a scan or an undo of a visit, was sent for a stored-money pass. Use the money action instead. |
BUSINESS_UNAVAILABLE |
403 | no_retry |
The business can't use stored-money operations now. |
MONETARY_SALES_DISABLED |
403 | no_retry |
Stored-money sales aren't switched on, ready or allowed for this business or pass. |
MONETARY_TEMPLATE_LIMIT |
403 | no_retry |
The business reached its limit of stored-money products. |
SALE_UNAVAILABLE |
403 | no_retry |
The sale can't be used here, for example the member is inactive or the product changed. |
MEMBER_UNAVAILABLE |
404 | no_retry |
The member can't receive a stored-money pass. |
ACCOUNT_RESTRICTED |
409 | no_retry |
The pass account is restricted, voided or under integrity review, or its member is inactive. |
REFUND_PENDING |
409 | contact_support |
A refund is pending on the account, so it can't change until a person settles the refund in Reward Loyalty. |
TERMS_CHANGED |
409 | fix_request |
The terms version sent with the sale isn't the current one. Read the current terms and send the sale with their version. |
SALE_REFERENCE_ALREADY_RECORDED |
409 | no_retry |
A sale with this external reference already exists. |
SALE_ALREADY_RESOLVED |
409 | no_retry |
The sale was already claimed or closed. |
SPEND_REFERENCE_ALREADY_RECORDED |
409 | no_retry |
A spend with this external reference already exists on the account. |
BALANCE_CHANGED |
409 | fix_request |
The balance isn't the expected_balance_minor the call sent. Read the balance again, check the spend still makes sense, and send it with the current balance. |
INSUFFICIENT_BALANCE |
409 | no_retry |
The pass balance is lower than the amount to spend. The member's own reward redemption uses the points code of the same name. |
BALANCE_CONFLICT |
409 | no_retry |
The entry would take the balance below zero or above the pass's starting balance. |
TRANSACTION_ALREADY_REVERSED |
409 | no_retry |
The spend was already reversed. |
OPERATION_UNAVAILABLE |
409 | no_retry |
The earlier result for this operation key isn't available. |
RECORD_REMOVED |
410 | no_retry |
A privacy removal erased the earlier result. Never replace its key to repeat the money action. |
Related topics
- Error handling: the same envelope for the Agent API, with a code-by-code view for HTTP callers.
- Troubleshooting
- Approvals