Idempotency
Safe retry of mutating API calls using Idempotency-Key with 24-hour replay windows
Idempotency
Network failures and timeouts can leave you uncertain whether a mutating request (payment creation, refund, etc.) succeeded. 505pay supports idempotent requests via the Idempotency-Key header so you can safely retry without risk of duplicate operations.
How idempotency works
- Generate a unique idempotency key for each logical operation.
- Include the key in the
Idempotency-Keyheader of your request. - If the request succeeds, the response is cached server-side for 24 hours.
- If you retry with the same key within the 24-hour window, you receive the same cached response — no duplicate operation is performed.
- After 24 hours the key expires and a new request with that key creates a fresh operation.
Key format
Idempotency-Key: <key>| Requirement | Detail |
|---|---|
| Length | 22–64 characters |
| Characters | Alphanumeric (a–z, A–Z, 0–9), hyphen (-), underscore (_) |
| Uniqueness | Must be unique per operation; reuse only when retrying the exact same logical action |
| Generation | Use UUIDv4, ULID, or a cryptographically random string |
Do not reuse idempotency keys across different operations. Reusing a key for a different payload returns the cached response from the first request, ignoring the new payload.
Partial failure semantics
If a request fails mid-flight (network error, server crash) before a response is sent, retrying with the same idempotency key returns the same error — not a new attempt.
This prevents double-execution in scenarios like:
- Payment service accepted the charge but the response was lost in transit
- Your server crashed after sending the request but before receiving the response
The key behavior: the server commits to either success or the recorded error. There is no "try again differently with the same key" — to attempt the operation under different conditions, use a new key.
Example: creating a payment
# Generate a key once and store it before sending
IDEMPOTENCY_KEY=$(openssl rand -hex 16)
curl -X POST https://sandbox.505pay.link/v1/payments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"amount": 150000,
"currency": "IDR",
"method": "virtual_account",
"bank": "BCA",
"customer": {
"name": "Budi Santoso",
"email": "[email protected]"
}
}'import { randomUUID } from 'crypto';
async function createPaymentSafely(payload: PaymentRequest): Promise<Payment> {
// Generate idempotency key once — persist it with the operation record
// so you can retry using the SAME key if the call fails
const idempotencyKey = randomUUID(); // UUIDv4 is valid (36 chars, alphanumeric + hyphens)
const res = await fetch('https://sandbox.505pay.link/v1/payments', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${err.error}: ${err.message}`);
}
return res.json();
}import uuid
import requests
def create_payment_safely(payload: dict, access_token: str) -> dict:
# Generate key before the request — store alongside the pending operation
idempotency_key = str(uuid.uuid4())
response = requests.post(
'https://sandbox.505pay.link/v1/payments',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Idempotency-Key': idempotency_key,
},
json=payload,
timeout=30,
)
response.raise_for_status()
return response.json()Retry pattern with idempotency
async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts = 3,
): Promise<T> {
// Key is fixed for this logical operation — generated BEFORE first attempt
const idempotencyKey = randomUUID();
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(idempotencyKey);
} catch (err) {
if (attempt === maxAttempts) throw err;
// Exponential backoff with jitter
const backoff = Math.min(2 ** attempt, 30) * 1000;
const jitter = backoff * (0.5 + Math.random());
await new Promise(r => setTimeout(r, jitter));
}
}
throw new Error('Unreachable');
}Idempotency keys are supported on all POST, PUT, and PATCH endpoints that create or mutate resources. GET and DELETE requests are inherently idempotent and do not require a key.
Was this page helpful?