Skip to content
StraventaDocs
Guides

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

  1. Generate a unique idempotency key for each logical operation.
  2. Include the key in the Idempotency-Key header of your request.
  3. If the request succeeds, the response is cached server-side for 24 hours.
  4. If you retry with the same key within the 24-hour window, you receive the same cached response — no duplicate operation is performed.
  5. After 24 hours the key expires and a new request with that key creates a fresh operation.

Key format

Idempotency-Key: <key>
RequirementDetail
Length22–64 characters
CharactersAlphanumeric (a–z, A–Z, 0–9), hyphen (-), underscore (_)
UniquenessMust be unique per operation; reuse only when retrying the exact same logical action
GenerationUse 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?

On this page