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:

  1. First request: The API processes the request and stores the response
  2. Duplicate request (same key + same body): Returns the stored response immediately
  3. Conflicting request (same key + different body): Returns a 409 Conflict error

Idempotency keys expire after 24 hours from creation.

Endpoints That Require Idempotency Keys

The following endpoints require an idempotency-key header:

EndpointMethodDescription
/v1/usersPOSTCreate a user
/v1/users/{userId}/verificationsPOSTCreate a verification
/v1/users/{userId}/virtual-accountsPOSTCreate a virtual account
/v1/users/{userId}/walletsPOSTCreate a wallet
/v1/recipientsPOSTCreate a recipient
/v1/payoutsPOSTCreate a payout
/v1/batch-payoutsPOSTCreate batch payouts

Using Idempotency Keys

Header Format

Include the idempotency key in the request headers:

idempotency-key: YOUR_UNIQUE_KEY

Key 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/Linux

2. 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-key header 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: