Idempotency Guide
Quick Reference
In payment processing, a non-idempotent retry is a financial error:
- Your server sends a charge request
- The charge succeeds, but the response is lost due to a network timeout
- Your server retries — without an idempotency key, A55 creates a second charge
- The customer is billed twice
Idempotency keys solve this: if A55 receives a duplicate key, it returns the original response instead of creating a new charge. This makes retries safe — the customer is charged exactly once regardless of how many times you send the request.
How It Works
Send an Idempotency-Key header with every POST request:
POST /api/v1/bank/wallet/charge/ HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
A55 stores the key and its associated response. If the same key is sent again within the expiration window:
- Same payload → Returns the original response (HTTP 200/201)
- Different payload → Returns HTTP
409 Conflict
Key Format
Use UUID v4 for idempotency keys:
550e8400-e29b-41d4-a716-446655440000
Requirements:
- Unique per business operation (not per retry attempt)
- Generated once and reused across all retry attempts for the same operation
- Minimum 16 characters, maximum 128 characters
Supported Endpoints
All POST endpoints support idempotency:
| Endpoint | Use Case |
|---|---|
POST /api/v1/bank/wallet/ | Wallet creation |
POST /api/v1/bank/wallet/charge/ | Charge creation |
POST /api/v1/bank/wallet/charge/refund/ | Refund |
POST /api/v1/bank/wallet/charge/capture/ | Capture |
POST /api/v1/bank/wallet/charge/cancel/ | Cancellation |
GET, PUT, and DELETE requests are naturally idempotent and do not require the header.
Duplicate Request Behavior
| Scenario | Result |
|---|---|
| Same key + same payload | Returns original response |
| Same key + different payload | HTTP 409 Conflict |
| Same key after expiration (24h) | Treated as a new request |
| No key provided | Each request creates a new resource |
Key Expiration
Idempotency keys expire after 24 hours. After expiration, the same key can be reused for a new request. Design your retry strategy to complete well within this window.
Code Examples
- cURL
- Python
- Node.js
# Generate an idempotency key ONCE for this operation
IDEMPOTENCY_KEY=$(uuidgen)
# First attempt
curl -s -X POST "https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/" \
-H "Authorization: Bearer ${A55_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d '{
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": { "number": "4111111111111111", "holder_name": "TEST", "expiration_month": "12", "expiration_year": "2030", "cvv": "123" },
"payer": { "name": "Test", "email": "test@example.com", "document": "12345678909", "document_type": "CPF" }
}'
# Retry with SAME key — returns the same response, no duplicate charge
curl -s -X POST "https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/" \
-H "Authorization: Bearer ${A55_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d '{
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": { "number": "4111111111111111", "holder_name": "TEST", "expiration_month": "12", "expiration_year": "2030", "cvv": "123" },
"payer": { "name": "Test", "email": "test@example.com", "document": "12345678909", "document_type": "CPF" }
}'
import os
import uuid
import time
import random
import requests
def create_charge_idempotent(charge_data: dict, max_retries: int = 3):
idempotency_key = str(uuid.uuid4())
headers = {
"Authorization": f"Bearer {os.environ['A55_ACCESS_TOKEN']}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
for attempt in range(max_retries + 1):
try:
response = requests.post(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
headers=headers,
json=charge_data,
)
if response.status_code == 409:
raise ValueError(f"Idempotency conflict: {response.json()}")
if response.status_code < 500:
return response.json()
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
except requests.ConnectionError:
if attempt == max_retries:
raise
time.sleep(2 ** attempt)
raise Exception("Max retries exceeded")
charge = create_charge_idempotent({
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": {
"number": "4111111111111111",
"holder_name": "TEST",
"expiration_month": "12",
"expiration_year": "2030",
"cvv": "123",
},
"payer": {
"name": "Test",
"email": "test@example.com",
"document": "12345678909",
"document_type": "CPF",
},
})
const crypto = require("crypto");
async function createChargeIdempotent(chargeData, maxRetries = 3) {
const idempotencyKey = crypto.randomUUID();
const headers = {
Authorization: `Bearer ${process.env.A55_ACCESS_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
{ method: "POST", headers, body: JSON.stringify(chargeData) }
);
if (res.status === 409) {
throw new Error(`Idempotency conflict: ${await res.text()}`);
}
if (res.status < 500) {
return await res.json();
}
const delay = (2 ** attempt + Math.random()) * 1000;
await new Promise(r => setTimeout(r, delay));
} catch (err) {
if (err.message.includes("Idempotency")) throw err;
if (attempt === maxRetries) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
throw new Error("Max retries exceeded");
}
createChargeIdempotent({
amount: "100.00",
currency: "BRL",
type_charge: "credit_card",
card: {
number: "4111111111111111",
holder_name: "TEST",
expiration_month: "12",
expiration_year: "2030",
cvv: "123",
},
payer: {
name: "Test",
email: "test@example.com",
document: "12345678909",
document_type: "CPF",
},
});
Never generate a new key per retry
The idempotency key must be generated once per business operation. If you generate a new key for each retry attempt, you lose duplicate protection entirely.