Errors, retries and idempotency
How the Vezmo API reports errors, covering the error body, requestId and X-Request-Id, what each status means, the error codes by area, which calls take an Idempotency-Key and how it behaves, when and how to retry, two GET routes not to repeat, and paging through lists with page or cursor.
Overview
Every answer from the API has the same shape, success or not. When a call fails, the HTTP status says what kind of
problem it is, message says it in one line, and errors lists each problem with a code. This page covers what to do
with them, which calls are safe to repeat, and how to page through lists.
The error body
An error answers with success: false and these fields. Examples:
Errors in the API reference.
- success
falseon every error.- message
- A short summary you can show or log. An unexpected error answers
500with a generic message and the codeinternal_server_error. - errors
- One entry per problem, each with a
codeand amessage. Some entries carry more, likerequired(the permissions a call needs),limitandretryAfter(a rate limit), orreason(which declined-card limit). - data
nullon an error.- requestId
- The id of this call. Quote it when you contact support.
- timestamp
- When we answered, in ISO 8601 UTC.
Branch on the HTTP status and on errors[0].code. Show message to a person if you like, but don't match on its words:
they can change.
Request ids
Every answer, a success too, carries requestId. To use your own (for example the id your logs already give the
request), send it in an X-Request-Id header: it comes back as requestId in the body. It isn't sent back as a
response header. Log it beside the call, and give it to support when a call misbehaves.
Status codes
| Status | What it means | What to do |
|---|---|---|
200 or 201 | It worked. | — |
400 | A field is missing or invalid, or the call can't be done in the object's state. | Fix the request; errors names each field. Don't send it again unchanged. |
401 | The access token is missing, wrong or expired, or the key stopped working. | Sign in again once, then retry once. See When a key stops working. |
403 | The key lacks a permission, the call isn't open to API keys, a test key tried a live-only change, or a plan limit refused it. | Don't retry. Read message and errors[0]. |
404 | Nothing with that id, in this key's mode. | Check the id and the key's mode (a test key can't see live objects). |
409 | The call clashes with the object's state: it is already paid, a payment on it is under way, or it changed a moment ago. | Read the object again, then decide. |
410 | A one-time token or a checkout session token has expired or was used. | Start again with a new one. |
422 | An Idempotency-Key was reused with a different body, or an amount is over what can be refunded. | Don't retry with the same key. |
429 | A rate limit, a plan limit or a declined-card limit. | Wait, then retry with backoff. See Rate limits. |
500 and above | Something failed on our side. | Retry with backoff. If it keeps failing, send support the requestId. |
Error codes
errors[0].code holds a code. Most errors use the status as the code, like http.400 or http.404, with the reason in
message; the ones below have a code of their own. Codes are case-sensitive: the plan and payment link codes are in
capitals.
Any call
| Code | Status | Meaning |
|---|---|---|
http.400, http.401, … | Any | The status as the code, with the reason in message. |
validation.… | 400 | A field failed a check. There is one entry per failing check, and the code repeats its message, like validation.amount must not be less than 0.01. Read the field from message; don't match the whole code. |
internal_server_error | 500 | Something failed on our side. |
A 403 with the message "Insufficient API permissions" lists what the call needs in errors[0].required. A key can't
be given more permissions: generate a new key, which gets every current one. See
Permissions.
Limits
| Code | Status | Meaning |
|---|---|---|
rate_limited | 429 | Too many requests with this API key. errors[0].retryAfter and the Retry-After header say how many seconds to wait. |
http.429 | 429 | Too many requests to one endpoint from your server's address. |
PLAN_LIMIT | 403 or 429 | Past a limit of your plan: 429 for API requests a minute, 403 for the others. errors[0] names the limit, what was used, what the plan allowed, the plan and the plan to upgradeTo. |
too_many_declines | 429 | Too many declined cards. errors[0].reason says which limit: checkout (this payment), card (this card) or network (your customer's network). Nothing was charged. Show your customer the message; don't retry the card. |
How each limit works: Rate limits.
Checkout sessions and payments
| Code | Status | Meaning |
|---|---|---|
http.422 | 422 | "Idempotency key was reused with different parameters." |
http.409 | 409 | The session's payment is already paid ("Payment is in terminal state …"). The order is paid: never retry it under a new key. |
http.403 | 403 | A live key while VezmoPay isn't approved for your business. Test keys aren't affected. |
http.400 | 400 | An app.returnUrl that isn't one of your registered return links. Add the app under Developers › Mobile apps. |
already_paid | 409 | Your customer's checkout was already paid. |
payment_in_progress | 409 or 429 | A payment on this is under way, or was just made. Wait a moment, then read the payment. |
method_unavailable | 400 | That way to pay isn't offered for this payment. |
return_url_required | 400 | A pay-later method was chosen in card fields without a returnUrl. |
return_url_not_registered | 400 | The return link to your app isn't registered for your business. |
local_price_changed | 409 | The price held in your customer's own currency ran out. Load the checkout again and let them choose again. |
A declined card answers 400 with the bank's reason as the code, like card_declined, and the message in our words.
The same decline reaches your server as payment.failed, with reason, declineCode and a decline object: see
Webhooks.
Payment links, invoices and bank payments
| Code | Status | Meaning |
|---|---|---|
PAYLINK_PAID | 400 | The payment link has already been paid. |
PAYLINK_INACTIVE | 400 | The payment link is switched off. |
PAYLINK_EXPIRED | 400 | The payment link has expired. |
PAYLINK_CLOSED | 400 | The payment link can no longer be paid. |
just_paid | 409 | A customer has just paid this. Read it again to see the payment. |
bank_payment_pending | 409 | A customer's bank payment on it is still settling (or waits for their bank check), so it can't be changed until that finishes. |
bank_verification_pending | 409 | A bank payment for it waits for your customer to verify their bank account, from the link we emailed them. |
Idempotency
Three calls take an Idempotency-Key header, so a retry after a timeout can't create a second payment, refund or
subscription:
| Call | The key is kept |
|---|---|
POST /merchant/secure-payments (create a checkout session) | Per teammate whose key makes the call, across test and live together. |
POST /merchant/payment/:id/refund | Per payment. |
POST /merchant/subscription | Per business, separately in test and in live. |
The key is 1 to 255 printable ASCII characters (anything else answers 400); your order number makes a good one. How
it answers:
- The same key and the same body: the first call's result. For a checkout session, the same payment with a new
urland client token. For a refund, the refund already made, never a second one. - The same key with another body:
422. - A checkout session's key after its payment was paid:
409. The order is paid.
Because session keys are shared by test and live, a key you used in a test run answers 422 or 409 in live. Give
test runs their own keys. More on this: Your order number.
Every other call takes no key. If one of those times out, read before you try again: for example, list your invoices and look for the one you were creating before you create it again.
When to retry
- Retry
429,500and above, and network errors or timeouts on the calls that are safe to repeat: reads, and the three calls above with the sameIdempotency-Key. - Wait first. After a
429, wait the seconds inRetry-After(orerrors[0].retryAfter). Otherwise back off: about 1 second, then 2, then 4, with a little randomness, and stop after a few tries. - On
401, sign in again once and retry once. A second401means the key itself stopped working. - Don't retry other
4xxanswers unchanged: they fail the same way. Fix the request, or read the object (409). - Never answer a
409from creating a checkout session with a new key: the order is already paid, and a new key starts a second payment.
Two reads that do something
Two GET routes change things, so a retry, a link checker or a prefetch repeats what they do:
GET /merchant/invoice/send-invoice/:idemails the invoice to the client again. One email per invoice a minute.GET /merchant/proposal/duplicate-proposal/:idmakes a new copy of the proposal on every call.
Call them once, on purpose. Don't retry them on a timeout without checking first: look at the invoice's activity, or list your proposals for the copy.
Pagination
Lists take page (from 1) and limit. The items are in data.data, and data.pagination holds total, page,
limit, totalPages, nextPage and prevPage. nextPage is the full address of the next page: follow it until it
is null. Examples: Pagination and limits.
Payments (GET /merchant/payment) also page by cursor, which stays right while new payments arrive:
- The answer carries
nextCursorandhasMore. Sendcursorwith the lastnextCursorto get the next page, untilhasMoreisfalse. limitis at most 100.totalstops counting at 10,000;totalIsCappedistruewhen there are more.- When you page by cursor, follow
nextCursor, notnextPage.
Two lists answer in their own shape:
- Subscriptions (
GET /merchant/subscription) taketake(up to 100) andskip, and answerdata.itemsanddata.total. - Agreements (
GET /merchant/agreements) answerdata.datawithtotal,page,limitandtotalPagesbeside it, and nonextPage: ask for the next page until you reachtotalPages.
Related
Facts checked against the product on 9 Nov 2026.
Was this page helpful?
WooCommerce
Take payments in a WooCommerce store with the VezmoPay plugin, covering installing it, Connect with VezmoPay (and the API key it replaces), the three integration modes, test and live, manual setup, how orders are completed, refunds, and what the plugin doesn't support yet.
Rate limits
The limits on how often you can call the Vezmo API, covering each API key's requests a second in test and live, your plan's API requests a minute in watch and enforced mode, limits per endpoint from one address, declined-card limits, email limits, and how to handle a 429 and Retry-After.