Conventions
Money, IDs, idempotency, pagination and errors — the rules every request follows.
JSON
{
"object": "transfer",
"id": "transfer_01K5X8QT8D4RFMJ2NVWXZ9QBCH",
"amount": "1000.00",
"currency": "USD",
"created": 1789041600,
"livemode": true
}Money is a string#
| Do | Don't |
|---|---|
"amount": "1000.00" | "amount": 1000 |
Floats lose cents. Parse into a decimal type — decimal.js, Python Decimal, BigDecimal.
IDs are prefixed and sortable#
transfer_01K5X8QT8D4RFMJ2NVWXZ9QBCH — object type, then a time-ordered ULID.
- The prefix tells you the type.
- Newer IDs sort after older ones. Use them as pagination cursors.
Time is Unix seconds#
"created": 1789041600 — an integer. Never ISO-8601.
Every object has#
| Field | |
|---|---|
object | Type discriminator. Switch on this |
id | Prefixed ID |
created | Unix seconds |
livemode | true for live keys |
Field names are snake_case. Attach your own references in metadata.
Idempotency#
Retry any write safely. It runs exactly once. Required on every POST.
HTTP
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890- 16–64 characters. Use a UUID.
- Scoped to your key. Expires after 24 hours.
- JSON key order doesn't count as a change.
Responses#
| You send | You get |
|---|---|
| Same key, same body | Original response + Idempotent-Replayed: true |
| Same key, different body or endpoint | 409 idempotency_key_reuse |
| Same key, first request still running | 409 resource_locked — retry shortly |
| No key | 400 parameter_empty |
| Key too short / long | 400 parameter_min_length_invalid / parameter_max_length_invalid |
Completed 4xx responses replay as-is. A 5xx releases the key, so a retry runs again.
Retry pattern#
Node.js
const key = crypto.randomUUID(); // once per operation, not per attempt
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch('https://api.withsuperpay.com/v1/transfers', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SUPERPAY_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(transfer),
});
const body = await res.json();
const locked = body.errors?.[0]?.code === 'resource_locked';
if (res.status < 500 && !locked) return body;
} catch {
// network error — safe to retry with the same key
}
await new Promise(r => setTimeout(r, 250 * 2 ** attempt));
}Pagination#
Cursor-based, on every list endpoint.
HTTP
GET /v1/transfers?limit=25&starting_after=transfer_01K5X8QT8D4RFMJ2NVWXZ9QBCHlimitinteger1–100. Default 10.
starting_afterstringID to page forward from.
ending_beforestringID to page backward from. Don't combine with starting_after.
Response
{ "object": "list", "data": [ ... ], "has_next": true }No total count — loop until has_next is false.
Fetch everything#
Node.js
async function* allTransfers() {
let cursor;
while (true) {
const qs = new URLSearchParams({ limit: '100', ...(cursor && { starting_after: cursor }) });
const page = await superpay(`/v1/transfers?${qs}`);
yield* page.data;
if (!page.has_next) return;
cursor = page.data.at(-1).id;
}
}Errors#
One shape. Every problem at once.
JSON
{
"object": "error",
"errors": [
{
"code": "parameter_empty",
"message": "You did not provide a value for amount.",
"metadata": { "field_name": "amount" }
}
],
"http_status_code": 400
}errorsis an array — fix all fields in one pass.- Branch on
code.messageis for humans and may change. metadata.field_namepoints at the offending field.
Codes#
| Code | HTTP | Meaning |
|---|---|---|
parameter_empty | 400 | Required field missing |
parameter_value_invalid | 400 | Not an accepted value |
parameter_min_length_invalid | 400 | Too short |
parameter_max_length_invalid | 400 | Too long |
parameter_range_invalid | 400 | Outside the allowed range |
parameter_unexpected | 400 | Unknown field |
request_body_invalid | 400 | Malformed JSON |
request_query_parameter_invalid | 400 | Bad query parameter |
secret_key_invalid | 401 | Bad or missing API key |
action_unauthorized | 401 · 403 | Not permitted |
object_not_found | 404 | No such object |
idempotency_key_reuse | 409 | Key reused with a different request |
resource_locked | 409 | Another request holds this object |
When to retry#
| Status | Retry |
|---|---|
400 401 403 404 | No — fix the request |
409 resource_locked | Yes, same key, with backoff |
409 idempotency_key_reuse | No — use a new key for a new request |
5xx · network error | Yes, same key, with backoff |