API fundamentals
Idempotency and retries
Retry a request after a timeout without creating a payment or a refund twice.
Networks fail. A request can time out after Raïs has already created the payment, and a client that sends it again would create a second one. An idempotency key makes the retry safe: the same key with the same request returns the original answer, and nothing happens twice.
Send a key
Add an Idempotency-Key header to POST /payments and POST /payments/{id}/refunds.
curl https://api.rais.app/v1/payments \
-H "Authorization: Bearer rais_sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"amount": 500000,
"reference": "order-1042"
}'The key is any value of 255 printable ASCII characters or fewer. A random UUID works. A key derived from your own record, such as order-1042, gives you "one payment per order" for free. Generate the key before the first attempt and keep it with the order, so every retry reuses it.
What happens when you repeat a request
| You send | Raïs answers |
|---|---|
| The same key and the same request | The original answer, with the original status code and the header Idempotent-Replayed: true. Nothing is created or refunded again. The content is the same; the keys of the JSON object may come in a different order. |
| The same key and a different request | 422 with idempotency_key_reused. |
| The same key while the first request is still running | 409 with idempotency_in_progress and a Retry-After header. Retry in a moment. |
| A key longer than 255 characters or with unusual characters | 400 with invalid_idempotency_key. |
A few details that matter:
- Same request means the same method, the same path and the same JSON body. Spacing and the order of keys do not matter.
- Only successful requests are remembered. A request that failed leaves nothing behind, so you can fix the problem and send it again with the same key.
- Keys live 24 hours. After that, the same key starts a new operation.
- Sandbox and live are separate. The same text used with a sandbox key and with a live key makes two different keys. Keys are also scoped to your store: two stores can use the same text without ever meeting.
- A replay is a snapshot. It returns the answer the first request got, for example a
pendingpayment, even if the payment has been paid since. Retrieve the payment to see its current state. - A crashed request is not a duplicate. If a request dies half-way, repeating it finds what it created instead of creating it again.
Retry in code
Retry on network errors, 429, 409 idempotency_in_progress, 500 and 503, with exponential backoff, and always with the same key.
async function createPayment(body, idempotencyKey) {
for (let attempt = 1; attempt <= 4; attempt++) {
try {
const response = await fetch("https://api.rais.app/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAIS_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey, // the same on every attempt
},
body: JSON.stringify(body),
});
const retryable = response.status === 429 || response.status === 409 || response.status >= 500;
if (!retryable || attempt === 4) return response;
const wait = Number(response.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
} catch (error) {
if (attempt === 4) throw error; // a network error: the request may or may not have arrived
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}
}Reads and cancellations
GET requests are safe to repeat as they are. Cancelling a payment twice answers 409 payment_not_cancellable the second time: treat a payment that is already cancelled as done.
Webhooks are at least once
The other half of reliability is on your side of the line: Raïs delivers each webhook until you acknowledge it, so the same event can arrive twice. De-duplicate on the event id. See Webhooks.