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": {
"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. 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.messageto the person who can fix it; it is written to be read. - On
429, wait the number of seconds inRetry-After, then retry. - On
5xxor a network failure, retry the same request with the sameIdempotency-Key: you get the original result if the first attempt went through. The one exception is500 REFUND_NOT_RECORDED: the refund went out, so look it up instead. - Don't retry other
4xxerrors unchanged: fix the request first. - Log the
Request-Idresponse header (req_…) with every error. It finds the exact request in Settings → Developers → Request log, and it's what support will ask for.
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 does all of this for you, with a typed error class per kind of error.