Idempotency
Overview
Idempotency keys ensure that API requests can be safely retried without causing duplicate operations. If a request fails due to a network error or timeout, you can retry it with the same idempotency key to get the same result without creating duplicate resources.
How It Works
When you make a request with an idempotency key:
- First request: The API processes the request and stores the response
- Duplicate request (same key + same body): Returns the stored response immediately
- Conflicting request (same key + different body): Returns a
409 Conflicterror
Idempotency keys expire after 24 hours from creation.
Endpoints That Require Idempotency Keys
The following endpoints require an idempotency-key header:
| Endpoint | Method | Description |
|---|---|---|
/v1/users | POST | Create a user |
/v1/users/{userId}/verifications | POST | Create a verification |
/v1/users/{userId}/virtual-accounts | POST | Create a virtual account |
/v1/users/{userId}/wallets | POST | Create a wallet |
/v1/recipients | POST | Create a recipient |
/v1/payouts | POST | Create a payout |
/v1/batch-payouts | POST | Create batch payouts |
Using Idempotency Keys
Header Format
Include the idempotency key in the request headers:
idempotency-key: YOUR_UNIQUE_KEYKey requirements:
- Must be a valid UUID (version 4 recommended)
- Must be unique per request
- Can be reused safely if the request fails
Example: Creating a User
curl -X POST https://api.balampay.com/v1/users \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{
"type": "individual",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"phone_number": "+14155552671",
"address": {
"street_line_1": "123 Main St",
"street_line_2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
}
}'Idempotency Behavior
Same Key, Same Request Body
If you send the same idempotency key with the same request body, the API returns the cached response from the first request:
# First request - creates the user
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-H "Authorization: Bearer TOKEN" \
-H "x-api-key: API_KEY" \
-d '{"type": "individual", "email": "[email protected]", ...}'
# Response: 201 Created
{
"id": "user_abc123",
"type": "individual",
"email": "[email protected]",
...
}
# Second request - same key, same body
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-H "Authorization: Bearer TOKEN" \
-H "x-api-key: API_KEY" \
-d '{"type": "individual", "email": "[email protected]", ...}'
# Response: 200 OK (same user, not created again)
{
"id": "user_abc123",
"type": "individual",
"email": "[email protected]",
...
}Same Key, Different Request Body
If you send the same idempotency key with a different request body, the API returns a 409 Conflict error:
# First request
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{"type": "individual", "email": "[email protected]", ...}'
# Second request - same key, different email
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{"type": "individual", "email": "[email protected]", ...}'
# Response: 409 Conflict
{
"code": "idempotency_key_reused",
"message": "This idempotency key has been used with different request data"
}Error Handling
Missing Idempotency Key
If you don't include an idempotency key on a required endpoint:
Response: 400 Bad Request
{
"code": "validation_error",
"message": "Idempotency key is required"
}Invalid Idempotency Key Format
If the idempotency key is not a valid UUID:
Response: 400 Bad Request
{
"code": "validation_error",
"message": "Validation failed",
"details": [
{
"path": "idempotency-key",
"message": "Invalid uuid",
"code": "invalid_string"
}
]
}Idempotency Key Reused with Different Data
If you reuse a key with different request data:
Response: 409 Conflict
{
"code": "idempotency_key_reused",
"message": "This idempotency key has been used with different request data"
}Best Practices
1. Generate Unique UUIDs
Use UUID v4 for idempotency keys:
// Node.js
const { v4: uuidv4 } = require('uuid');
const idempotencyKey = uuidv4();
// Python
import uuid
idempotency_key = str(uuid.uuid4())
// Command line
uuidgen # macOS/Linux2. Store Keys with Request Data
Store the idempotency key alongside the request data in your application:
const createUserRequest = {
idempotencyKey: uuidv4(),
userData: {
type: 'individual',
email: '[email protected]',
// ... other fields
}
};
// Store in your database
await db.requests.create(createUserRequest);
// Make API call
const response = await fetch('https://api.balampay.com/v1/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': createUserRequest.idempotencyKey
},
body: JSON.stringify(createUserRequest.userData)
});3. Retry Failed Requests
Use the same idempotency key when retrying failed requests:
async function createUserWithRetry(userData, maxRetries = 3) {
const idempotencyKey = uuidv4();
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('https://api.balampay.com/v1/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': idempotencyKey // Same key for all retries
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}4. Don't Reuse Keys Across Different Operations
Each operation should have its own unique idempotency key:
// ❌ Wrong - reusing key for different operations
const key = uuidv4();
await createUser(userData, key);
await createVirtualAccount(accountData, key); // Don't do this!
// ✅ Correct - unique key for each operation
const userKey = uuidv4();
await createUser(userData, userKey);
const accountKey = uuidv4();
await createVirtualAccount(accountData, accountKey);5. Include Idempotency Keys in Logs
Log idempotency keys to help with debugging:
const idempotencyKey = uuidv4();
console.log({
action: 'create_user',
idempotencyKey,
email: userData.email,
timestamp: new Date().toISOString()
});
try {
const user = await createUser(userData, idempotencyKey);
console.log({ action: 'user_created', idempotencyKey, userId: user.id });
} catch (error) {
console.error({ action: 'user_creation_failed', idempotencyKey, error });
}6. Handle 409 Conflicts Appropriately
If you get a 409 Conflict, it means you accidentally reused a key with different data. Generate a new key:
async function createUserSafe(userData) {
let idempotencyKey = uuidv4();
try {
return await createUser(userData, idempotencyKey);
} catch (error) {
if (error.code === 'idempotency_key_reused') {
// Generate new key and retry
idempotencyKey = uuidv4();
return await createUser(userData, idempotencyKey);
}
throw error;
}
}Key Expiration
Idempotency keys expire 24 hours after creation. After expiration:
- The key can be reused for new requests
- The cached response is no longer available
# Day 1 - Create user
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{...}'
# Day 2 (25 hours later) - Same key can be used again
curl -X POST https://api.balampay.com/v1/users \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{...}' # Creates a new user (key expired)Common Patterns
Pattern 1: User Creation Flow
async function registerNewUser(email, firstName, lastName) {
const idempotencyKey = uuidv4();
// Store the key with user data
await db.pendingUsers.create({
email,
firstName,
lastName,
idempotencyKey,
status: 'pending'
});
try {
// Create user via API
const user = await fetch('https://api.balampay.com/v1/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': idempotencyKey
},
body: JSON.stringify({
type: 'individual',
email,
first_name: firstName,
last_name: lastName,
// ... other fields
})
}).then(r => r.json());
// Update status
await db.pendingUsers.update({ email }, {
status: 'created',
userId: user.id
});
return user;
} catch (error) {
// Log error but keep the pending record
await db.pendingUsers.update({ email }, {
status: 'failed',
error: error.message
});
throw error;
}
}Pattern 2: Verification Creation Flow
async function startVerification(userId) {
const idempotencyKey = uuidv4();
const verification = await fetch(
`https://api.balampay.com/v1/users/${userId}/verifications`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': idempotencyKey
},
body: JSON.stringify({
type: 'embedded-link',
redirect_uri: 'https://yourapp.com/verification-complete'
})
}
).then(r => r.json());
// Store verification URL
await db.users.update(userId, {
verificationUrl: verification.url,
verificationIdempotencyKey: idempotencyKey
});
return verification;
}Pattern 3: Virtual Account Creation Flow
async function createVirtualAccountForUser(userId, destinationAddress) {
const idempotencyKey = uuidv4();
const virtualAccount = await fetch(
`https://api.balampay.com/v1/users/${userId}/virtual-accounts`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': idempotencyKey
},
body: JSON.stringify({
type: 'US_ACH',
destination: {
currency: 'usdc',
network: 'solana',
address: destinationAddress
}
})
}
).then(r => r.json());
return virtualAccount;
}Summary
- Always include
idempotency-keyheader on POST requests - Use UUID v4 for generating keys
- Reuse the same key when retrying failed requests
- Don't reuse keys across different operations
- Store keys with request data for tracking
- Handle 409 conflicts by generating new keys
- Keys expire after 24 hours
For more information, see:
Updated 12 days ago
