Every error from /api/v1 is { "error": { "code", "message" } }. Branch on code, which never changes; show message, which is written for people and may be reworded. This page lists every code the API can return, taken from the API's own source; how to handle them in general is on Errors and retries.
By status
| Status | Codes |
|---|---|
| 400 | INVALID_JSON, INVALID_INPUT, INVALID_IDEMPOTENCY_KEY, NOT_EDITABLE, CUSTOMER_MISMATCH |
| 401 | UNAUTHORIZED, API_KEY_EXPIRED |
| 403 | NOT_ENROLLED, PERMISSION_DENIED, IP_NOT_ALLOWED, TEST_MODE_ONLY |
| 404 | NOT_FOUND, METER_NOT_FOUND, CUSTOMER_NOT_FOUND |
| 409 | IDEMPOTENCY_KEY_REUSED, NOTHING_TO_SEND, NOT_REFUNDABLE, REFUND_PENDING_APPROVAL, TERMS_URL_REQUIRED, INVOICE_CLOSED, DUPLICATE_CUSTOMER, CODE_TAKEN, LIMIT_REACHED, SUBSCRIPTION_ENDED, ALREADY_CANCELING, CANNOT_PAUSE, NOT_PAUSED, CHANGE_IN_PROGRESS, SWITCH_SCHEDULED |
| 422 | IDEMPOTENCY_KEY_REUSED, CUSTOMER_HAS_NO_EMAIL, TIMESTAMP_IN_FUTURE, TIMESTAMP_TOO_OLD, PERIOD_CLOSED |
| 429 | RATE_LIMITED, TOO_SOON |
| 500 | INTERNAL_ERROR, REFUND_NOT_RECORDED |
| 502 | PROCESSOR_ERROR, EMAIL_FAILED |
| 503 | NOT_CONFIGURED |
Every code
| Code | Status | Meaning | Fix | Retry | Returned by |
|---|---|---|---|---|---|
UNAUTHORIZED | 401 | The Authorization header is missing, or the key is malformed, unknown or revoked. A revoked key reads exactly like an unknown one. | Send Authorization: Bearer ak_live_… with an active key from Settings → Developers. | No: fix the request | Every request with a key |
API_KEY_EXPIRED | 401 | The key passed its expiry date, or the overlap after it was rolled ended. The message says when it expired. | Use the key that replaced it, or create a new one in Settings → Developers. See Authentication. | No: fix the request | Every request with a key |
NOT_ENROLLED | 403 | The key is valid, but the business isn't verified and set up for card payments yet. | Finish verification in Settings → Verification; the API opens once card payments are set up. | No: fix the request | Every request with a key |
PERMISSION_DENIED | 403 | The key doesn't have the permission this request needs. The message names it, such as refunds:write. | Use a key that has it: an owner or admin creates one in Settings → Developers. See Authentication. | No: fix the request | Every request with a key |
IP_NOT_ALLOWED | 403 | The key has an IP allowlist and this request came from another address, which the message names. | Call from a server on the allowlist, or an owner or admin edits the key's allowed IPs in Settings → Developers. | No: fix the request | Every request with a key |
RATE_LIMITED | 429 | The key made more than 100 requests this minute. | Wait the seconds in the Retry-After header, then retry. See Rate limits. | After Retry-After | Every request with a key |
INTERNAL_ERROR | 500 | Something failed on our side. | Retry with the same Idempotency-Key; you get the first result if the first attempt went through. Contact Support if it keeps happening. | Yes, same Idempotency-Key | Every request with a key |
INVALID_JSON | 400 | The body isn't valid JSON, or couldn't be read. An empty body reads as {}. | Send Content-Type: application/json and a JSON object. | No: fix the request | Every request with a body |
INVALID_INPUT | 400 | A field, filter, limit or cursor is missing or invalid. The message starts with the field's name, as in amountCents: …, or reads Invalid cursor. | Fix the named field. Don't retry the same request unchanged. | No: fix the request | Every list, every request with a body, and `GET /meters/{id}/usage` |
INVALID_IDEMPOTENCY_KEY | 400 | The Idempotency-Key header is empty, longer than 255 characters, or has characters other than printable ASCII. | Use a UUID. See Idempotency. | No: fix the request | Every request with an Idempotency-Key |
IDEMPOTENCY_KEY_REUSED | 422 or 409 | The Idempotency-Key was used in the last 24 hours with a different body or endpoint. Or: The usage event's idempotencyKey was already used for a different event. | Use a new key for a new request; reuse a key only to retry the same request. Send a new idempotencyKey for each event; repeat a key only to resend the same event. | No: fix the request | Every request with an Idempotency-Key, and `POST /meter-events` |
NOT_FOUND | 404 | No such object on your business (an id from another business reads the same), or no such endpoint under /api/v1, which still answers in JSON. | Check the id and the path. Ids are case-sensitive. | No: fix the request | Every request with an id in the path, and `POST /refunds`, `POST /test-helpers/events` |
TEST_MODE_ONLY | 403 | A test helper was called with a live key. Test helpers never touch live data. | Use a test key (ak_test_…): turn on Test mode in the dashboard and create one in Settings → Developers. | No: fix the request | `POST /test-helpers/events` |
NOTHING_TO_SEND | 409 | You have no test object of the kind this event is about yet (no paid test payment for payment.succeeded, no test invoice for invoice.paid…). | Make one in test mode first (pay a test link with a test card), or choose Load sample data in the test mode bar. | No: fix the request | `POST /test-helpers/events` |
NOT_CONFIGURED | 503 | The part of Anytime Capital Payments this needs isn't available right now. | Retry later, with the same Idempotency-Key on a write. | Yes, same Idempotency-Key | `POST /refunds`, `POST /subscriptions/{id}/cancel`, `POST /subscriptions/{id}/pause`, `POST /subscriptions/{id}/resume`, `GET /payouts`, `GET /payouts/{id}`, `GET /balance`, `GET /disputes`, `GET /disputes/{id}`, `POST /webhook-endpoints`, `POST /test-helpers/events` |
PROCESSOR_ERROR | 502 | The payments network didn't answer in time. | Retry with the same Idempotency-Key. A refund is keyed at the network by that key too, so a retry never refunds twice. | Yes, same Idempotency-Key | `POST /refunds`, `POST /subscriptions/{id}/cancel`, `POST /subscriptions/{id}/pause`, `POST /subscriptions/{id}/resume`, `GET /payouts`, `GET /payouts/{id}`, `GET /balance`, `GET /disputes`, `GET /disputes/{id}` |
NOT_REFUNDABLE | 409 | The payment can't be refunded in its current state, or it was paid in stablecoins, which settle straight to your wallet. | Check the payment's status and amountRefundedCents with GET /api/v1/payments/{id}. Refund a stablecoin payment by sending the tokens back from your wallet. | No: fix the request | `POST /refunds` |
REFUND_PENDING_APPROVAL | 409 | Another refund on this payment is already waiting for an owner's approval. | Wait for the owner's decision: GET /api/v1/refunds?paymentId=…&status=pending_approval shows it. | No: fix the request | `POST /refunds` |
REFUND_NOT_RECORDED | 500 | The refund was sent, but our record of it isn't there yet. | Don't refund again. List GET /api/v1/refunds?paymentId=… in a minute to find it. | No: fix the request | `POST /refunds` |
NOT_EDITABLE | 400 | The update names a field that is fixed once a link exists: amountCents, items, payWhatYouWant, minAmountCents, shippingCents, allowDiscountCodes or singleUse. The message names it. | Create a new link at the new price and deactivate the old one. | No: fix the request | `POST /payment-links/{id}` |
TERMS_URL_REQUIRED | 409 | requireTermsConsent needs a link to your terms, and the business hasn't set one. | Add your terms link in Settings → Payments, then retry. | No: fix the request | `POST /payment-links`, `POST /payment-links/{id}` |
INVOICE_CLOSED | 409 | The invoice is already paid, refunded or void, so there's nothing to send. | Read the invoice's status; nothing to do. | No: fix the request | `POST /invoices/{id}/send` |
TOO_SOON | 429 | The invoice was already emailed in the last 24 hours. | Wait the seconds in Retry-After. | After Retry-After | `POST /invoices/{id}/send` |
EMAIL_FAILED | 502 | We couldn't send the email. | Retry. Nothing was marked as sent. | Yes, same Idempotency-Key | `POST /invoices/{id}/send` |
DUPLICATE_CUSTOMER | 409 | Another saved customer already has this email address, in any letter case. | Find them with GET /api/v1/customers?email=… and update that customer instead. | No: fix the request | `POST /customers`, `PATCH /customers/{id}` |
CODE_TAKEN | 409 | The business already has a discount code with this code. | Pick another code, or use the existing one. | No: fix the request | `POST /discount-codes` |
LIMIT_REACHED | 409 | The business already has 10 webhook endpoints. | Delete one you no longer use with DELETE /api/v1/webhook-endpoints/{id}. | No: fix the request | `POST /webhook-endpoints` |
SUBSCRIPTION_ENDED | 409 | The subscription has already ended (canceled, expired or completed). | Nothing to do; a customer subscribes again at the plan's url. | No: fix the request | `POST /subscriptions/{id}/cancel`, `POST /subscriptions/{id}/pause`, `POST /subscriptions/{id}/resume` |
ALREADY_CANCELING | 409 | The subscription is already set to cancel at the end of its period. | Send atPeriodEnd: false to end it now, or POST …/resume to keep it. | No: fix the request | `POST /subscriptions/{id}/cancel` |
CANNOT_PAUSE | 409 | Only an active or trialing subscription that renews can be paused: this one is paused, past due or set to cancel. | Read the subscription's status and cancelAtPeriodEnd first. | No: fix the request | `POST /subscriptions/{id}/pause` |
NOT_PAUSED | 409 | The subscription is neither paused nor set to cancel, so there's nothing to resume. | Nothing to do. | No: fix the request | `POST /subscriptions/{id}/resume` |
CHANGE_IN_PROGRESS | 409 | The subscription is switching plans right now. | Retry in a minute. | Yes, same Idempotency-Key | `POST /subscriptions/{id}/cancel`, `POST /subscriptions/{id}/pause`, `POST /subscriptions/{id}/resume` |
SWITCH_SCHEDULED | 409 | The subscription switches plans at its next renewal, so it can't be paused or resumed. | Undo the switch from the subscriber's details on the Subscriptions page first. | No: fix the request | `POST /subscriptions/{id}/pause`, `POST /subscriptions/{id}/resume` |
METER_NOT_FOUND | 404 | No active meter has this eventName. | Check the meter's event name on the Meters tab of Subscriptions, or with GET /api/v1/meters. | No: fix the request | `POST /meter-events` |
CUSTOMER_NOT_FOUND | 404 | No saved customer has this customerId. | Send the customer's cus_… id, or customerEmail instead. | No: fix the request | `POST /meter-events` |
CUSTOMER_HAS_NO_EMAIL | 422 | The customer has no email address, and usage is matched to subscriptions by email. | Add an email to the customer in the dashboard, or send customerEmail. | No: fix the request | `GET /meters/{id}/usage`, `POST /meter-events` |
CUSTOMER_MISMATCH | 400 | customerId and customerEmail name different customers. | Send one of them. | No: fix the request | `GET /meters/{id}/usage`, `POST /meter-events` |
TIMESTAMP_IN_FUTURE | 422 | The event's timestamp is more than five minutes ahead of our clock. | Send the time the usage happened, or leave timestamp out for now. | No: fix the request | `POST /meter-events` |
TIMESTAMP_TOO_OLD | 422 | The event's timestamp is more than 400 days ago. | Usage that old can't be billed; drop the event. | No: fix the request | `POST /meter-events` |
PERIOD_CLOSED | 422 | The timestamp falls in a period already billed for this customer and meter. | Send usage from the end of the billed period on. The message says when that is. | No: fix the request | `POST /meter-events` |
Usage events in a batch
POST /api/v1/meter-events/batch answers 200 when the batch itself is valid, and reports each refused event in its result with the code a single event would get: INVALID_INPUT, METER_NOT_FOUND, CUSTOMER_NOT_FOUND, CUSTOMER_HAS_NO_EMAIL, CUSTOMER_MISMATCH, TIMESTAMP_IN_FUTURE, TIMESTAMP_TOO_OLD, PERIOD_CLOSED or IDEMPOTENCY_KEY_REUSED. See Meters.
MCP server errors
A tool call that reaches the API returns the API's error as the tool result, with isError: true: { status, error: { code, message } }, plus retryAfterSeconds when the API asked to wait. UNEXPECTED_RESPONSE means the API answered without a JSON body. Problems with the JSON-RPC message itself answer as JSON-RPC errors:
| Code | Meaning |
|---|---|
| -32700 | The body isn't valid JSON. |
| -32600 | Not a JSON-RPC 2.0 request, an empty batch, or an unsupported MCP-Protocol-Version header (HTTP 400). GET and DELETE answer 405 with this code. |
| -32601 | A method other than initialize, ping, tools/list or tools/call. |
| -32602 | An unknown tool, no params.name, or arguments that aren't an object (or a missing id). |
| -32603 | Something failed on our side; try again. |
Node SDK errors
The Node SDK throws a class per kind of error, each with the API's code, message, status and retryAfterSeconds, and adds codes of its own: TIMEOUT and CONNECTION_ERROR (no answer, after its retries), UNEXPECTED_RESPONSE (an answer without the error envelope) and SIGNATURE_VERIFICATION_FAILED (a webhook that didn't verify).