Successful requests answer 200, or 201 when something was created. Every error, on every endpoint, has the same shape: a stable code to branch on and a message written for people. Validation messages name the field that needs fixing.
{
"error": {
"code": "INVALID_INPUT",
"message": "amountCents: The smallest link is $1.00"
}
}Status codes
| Status | Codes | What happened |
|---|---|---|
| 400 | INVALID_INPUT, INVALID_JSON, NOT_EDITABLE, INVALID_IDEMPOTENCY_KEY | The request is malformed, or a field is invalid or can't be changed. The message names the field. |
| 401 | UNAUTHORIZED | The API key is missing, invalid or revoked. |
| 403 | NOT_ENROLLED | Card payments aren't set up for this business yet. |
| 404 | NOT_FOUND | No such object for your business, or no such endpoint. |
| 409 | INVOICE_CLOSED, TERMS_URL_REQUIRED | The request conflicts with the object's state or your settings. |
| 422 | IDEMPOTENCY_KEY_REUSED | The Idempotency-Key was already used with a different request. |
| 429 | RATE_LIMITED, TOO_SOON | Slow down: wait the number of seconds in the Retry-After header. |
| 5xx | INTERNAL_ERROR, EMAIL_FAILED | Something failed on our side. Retrying with the same Idempotency-Key is safe. |
Error codes
| Code | Meaning and what to do |
|---|---|
INVALID_INPUT | A field is missing or invalid, e.g. amountCents: The smallest link is $1.00. Fix the named field. |
INVALID_JSON | The body isn't valid JSON. Send Content-Type: application/json and a JSON object. |
NOT_EDITABLE | You tried to change a payment link's price or another field that is fixed once a link exists. Create a new link instead. |
INVALID_IDEMPOTENCY_KEY | The Idempotency-Key header is empty, too long or has unprintable characters. Use a UUID. |
UNAUTHORIZED | Send a live key as Authorization: Bearer ak_live_…. |
NOT_ENROLLED | Finish verification in the dashboard; the API opens once card payments are set up. |
NOT_FOUND | The id doesn't belong to your business, or the path isn't an endpoint. Unknown paths under /api/v1 answer this JSON too, never an HTML page. |
INVOICE_CLOSED | The invoice is already paid, refunded or void, so it can't be sent again. |
TERMS_URL_REQUIRED | requireTermsConsent needs a link to your terms in Settings → Payments first. |
IDEMPOTENCY_KEY_REUSED | Use a new Idempotency-Key for a new request. |
RATE_LIMITED | The key made too many requests this minute. Wait for Retry-After. |
TOO_SOON | The invoice was already emailed in the last 24 hours. Wait for Retry-After. |
EMAIL_FAILED | We couldn't send the email. Try again. |
INTERNAL_ERROR | Something went wrong on our side. Try again with the same Idempotency-Key. |
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. - Don't retry other
4xxerrors unchanged: fix the request first.
const API = "https://anytime-capital.com/api/v1";
export async function anytime(path, { method = "GET", body, idempotencyKey } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (res.ok) return data;
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt === 3) {
const error = new Error(`${data.error.code}: ${data.error.message}`);
error.code = data.error.code;
error.status = res.status;
throw error;
}
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}