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
false on every error.
message
A short summary you can show or log. An unexpected error answers 500 with a generic message and the code internal_server_error.
errors
One entry per problem, each with a code and a message. Some entries carry more, like required (the permissions a call needs), limit and retryAfter (a rate limit), or reason (which declined-card limit).
data
null on 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

StatusWhat it meansWhat to do
200 or 201It worked.—
400A 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.
401The 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.
403The 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].
404Nothing with that id, in this key's mode.Check the id and the key's mode (a test key can't see live objects).
409The 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.
410A one-time token or a checkout session token has expired or was used.Start again with a new one.
422An Idempotency-Key was reused with a different body, or an amount is over what can be refunded.Don't retry with the same key.
429A rate limit, a plan limit or a declined-card limit.Wait, then retry with backoff. See Rate limits.
500 and aboveSomething 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

CodeStatusMeaning
http.400, http.401, …AnyThe status as the code, with the reason in message.
validation.…400A 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_error500Something 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

CodeStatusMeaning
rate_limited429Too many requests with this API key. errors[0].retryAfter and the Retry-After header say how many seconds to wait.
http.429429Too many requests to one endpoint from your server's address.
PLAN_LIMIT403 or 429Past 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_declines429Too 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

CodeStatusMeaning
http.422422"Idempotency key was reused with different parameters."
http.409409The session's payment is already paid ("Payment is in terminal state …"). The order is paid: never retry it under a new key.
http.403403A live key while VezmoPay isn't approved for your business. Test keys aren't affected.
http.400400An app.returnUrl that isn't one of your registered return links. Add the app under Developers › Mobile apps.
already_paid409Your customer's checkout was already paid.
payment_in_progress409 or 429A payment on this is under way, or was just made. Wait a moment, then read the payment.
method_unavailable400That way to pay isn't offered for this payment.
return_url_required400A pay-later method was chosen in card fields without a returnUrl.
return_url_not_registered400The return link to your app isn't registered for your business.
local_price_changed409The 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.

CodeStatusMeaning
PAYLINK_PAID400The payment link has already been paid.
PAYLINK_INACTIVE400The payment link is switched off.
PAYLINK_EXPIRED400The payment link has expired.
PAYLINK_CLOSED400The payment link can no longer be paid.
just_paid409A customer has just paid this. Read it again to see the payment.
bank_payment_pending409A 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_pending409A 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:

CallThe 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/refundPer payment.
POST /merchant/subscriptionPer 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 url and 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, 500 and above, and network errors or timeouts on the calls that are safe to repeat: reads, and the three calls above with the same Idempotency-Key.
  • Wait first. After a 429, wait the seconds in Retry-After (or errors[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 second 401 means the key itself stopped working.
  • Don't retry other 4xx answers unchanged: they fail the same way. Fix the request, or read the object (409).
  • Never answer a 409 from 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/:id emails the invoice to the client again. One email per invoice a minute.
  • GET /merchant/proposal/duplicate-proposal/:id makes 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 nextCursor and hasMore. Send cursor with the last nextCursor to get the next page, until hasMore is false.
  • limit is at most 100.
  • total stops counting at 10,000; totalIsCapped is true when there are more.
  • When you page by cursor, follow nextCursor, not nextPage.

Two lists answer in their own shape:

  • Subscriptions (GET /merchant/subscription) take take (up to 100) and skip, and answer data.items and data.total.
  • Agreements (GET /merchant/agreements) answer data.data with total, page, limit and totalPages beside it, and no nextPage: ask for the next page until you reach totalPages.

Facts checked against the product on 9 Nov 2026.

Was this page helpful?