# Errors and retries

> Every Anytime Capital Payments API error has one JSON shape: a stable code to branch on and a message for people. How to handle them and retry safely.

Source: https://anytime-capital.com/docs/errors · Markdown: https://anytime-capital.com/docs/errors.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Successful requests answer `200`, or `201` when something was created (`202` for a refund waiting for an owner's approval). Every error, on every endpoint, has the same shape: a stable `code` to branch on and a `message` written for people. Validation messages start with the field that needs fixing.

**Error response**

```json
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "amountCents: The smallest link is $1.00"
  }
}
```

## Status codes

| Status | Codes | Retry? |
| --- | --- | --- |
| 400 | `INVALID_JSON`, `INVALID_INPUT`, `INVALID_IDEMPOTENCY_KEY`, `NOT_EDITABLE`, `CUSTOMER_MISMATCH` | No: fix the request |
| 401 | `UNAUTHORIZED`, `API_KEY_EXPIRED` | No: fix the request |
| 403 | `NOT_ENROLLED`, `PERMISSION_DENIED`, `IP_NOT_ALLOWED`, `TEST_MODE_ONLY` | No: fix the request |
| 404 | `NOT_FOUND`, `METER_NOT_FOUND`, `CUSTOMER_NOT_FOUND` | No: fix the request |
| 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` | No: fix the request; Yes, same Idempotency-Key |
| 422 | `IDEMPOTENCY_KEY_REUSED`, `CUSTOMER_HAS_NO_EMAIL`, `TIMESTAMP_IN_FUTURE`, `TIMESTAMP_TOO_OLD`, `PERIOD_CLOSED` | No: fix the request |
| 429 | `RATE_LIMITED`, `TOO_SOON` | After `Retry-After` |
| 500 | `INTERNAL_ERROR`, `REFUND_NOT_RECORDED` | Yes, same Idempotency-Key; No: fix the request |
| 502 | `PROCESSOR_ERROR`, `EMAIL_FAILED` | Yes, same Idempotency-Key |
| 503 | `NOT_CONFIGURED` | Yes, same Idempotency-Key |

What each code means, how to fix it and which endpoints return it: [Error code catalogue](https://anytime-capital.com/docs/error-codes.md). Each endpoint in the API reference also lists its own errors.

## Handling errors

- Branch on `error.code`, never on the message text, which may be reworded.
- Show `error.message` to the person who can fix it; it is written to be read.
- On `429`, wait the number of seconds in `Retry-After`, then retry.
- On `5xx` or a network failure, retry the same request with the same `Idempotency-Key`: you get the original result if the first attempt went through. The one exception is `500 REFUND_NOT_RECORDED`: the refund went out, so look it up instead.
- Don't retry other `4xx` errors unchanged: fix the request first.
- Log the `Request-Id` response header (`req_…`) with every error. It finds the exact request in **Settings → Developers → Request log**, and it's what support will ask for.

**A small client with retries (Node.js 18+)**

```javascript
import { randomUUID } from "node:crypto";

const API = "https://anytime-capital.com/api/v1";

export async function anytime(path, { method = "GET", body, idempotencyKey } = {}) {
  // One key for every attempt, so a retry can never act twice.
  const key = method === "GET" ? null : (idempotencyKey ?? randomUUID());
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(API + path, {
        method,
        headers: {
          Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
          ...(body ? { "Content-Type": "application/json" } : {}),
          ...(key ? { "Idempotency-Key": key } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
      });
    } catch (networkError) {
      if (attempt === 3) throw networkError;
      await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
      continue;
    }
    const data = await res.json().catch(() => ({}));
    if (res.ok) return data;

    const code = data.error?.code;
    const retryable = (res.status === 429 && code === "RATE_LIMITED") || (res.status >= 500 && code !== "REFUND_NOT_RECORDED");
    if (!retryable || attempt === 3) {
      const error = new Error(`${code}: ${data.error?.message}`);
      error.code = code;
      error.status = res.status;
      error.requestId = res.headers.get("Request-Id"); // find it in the request log
      throw error;
    }
    const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}
```

The [Node SDK](https://anytime-capital.com/docs/node-sdk.md) does all of this for you, with a typed error class per kind of error.

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
