# 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.

Source: https://vezmo.com/docs/developers/errors-and-retries

## 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](https://user.vezmo.com/docs/v1/errors).

- **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](https://user.vezmo.com/supports) 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](/docs/developers/api-keys#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](/docs/developers/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](/docs/developers/api-keys#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](/docs/developers/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](/docs/developers/webhooks#payments).

### 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
  `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](/docs/developers/hosted-checkout#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](https://user.vezmo.com/docs/v1/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`.

## Related

- [Rate limits](/docs/developers/rate-limits)
- [API keys and access tokens](/docs/developers/api-keys)
- [Webhooks](/docs/developers/webhooks)
- [Errors in the API reference](https://user.vezmo.com/docs/v1/errors)
