Guides

Idempotency

How to use idempotency keys to prevent duplicate operations in the BeeL. API.


What is Idempotency?

Idempotency ensures that an operation can be executed multiple times with the same result. In APIs, this is crucial to avoid creating duplicate resources when there are network issues or application errors.

How it Works

BeeL. implements idempotency via the standard Idempotency-Key header. When you include this header in write requests (POST, PUT), the API:

  1. Stores the idempotency key along with the operation result
  2. If it receives the same request with the same key, returns the stored result
  3. Does not execute the operation again

This prevents duplicate invoices, customers, or other resources even if your application retries the request.

POST Requests (creation)

Always use Idempotency-Key when creating resources:

curl -X POST "https://app.beel.es/api/v1/invoices" \
  -H "Authorization: Bearer beel_sk_xxx" \
  -H "Idempotency-Key: invoice-order-12345" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "STANDARD",
    "issue_date": "2025-01-15",
    "recipient": {
      "recipient_type": "EXISTING",
      "customer_id": "550e8400-e29b-41d4-a716-446655440000"
    },
    "lines": [
      {
        "description": "Consulting",
        "quantity": 1,
        "unit_price": 100.00
      }
    ]
  }'

PUT Requests (update)

Also recommended for critical updates:

curl -X PUT "https://app.beel.es/api/v1/customers/{customer_id}" \
  -H "Authorization: Bearer beel_sk_xxx" \
  -H "Idempotency-Key: update-client-address-456" \
  -H "Content-Type: application/json" \
  -d '{
    "address": {
      "street": "New address 123",
      "number": "45",
      "postal_code": "28001",
      "city": "Madrid",
      "province": "Madrid",
      "country": "España"
    }
  }'

Developer Plan exclusive: Idempotency support is available only with the Developer Plan. See Pricing for details.

Key Generation

The idempotency key must be unique for each logical operation you want to perform.

✅ Best Practices

// Use UUID v4
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4(); // "550e8400-e29b-41d4-a716-446655440000"

// Combine identifiers from your system
const idempotencyKey = `invoice-order-${orderId}-${timestamp}`;

// Use external references
const idempotencyKey = `sync-shopify-order-${shopifyOrderId}`;

// Hash-based (deterministic for same input)
import crypto from 'crypto';
const idempotencyKey = crypto
  .createHash('sha256')
  .update(JSON.stringify({ orderId, customerId }))
  .digest('hex');

❌ Bad Practices

// DON'T use values that repeat across different operations
const idempotencyKey = 'create-invoice'; // ❌ Too generic, will collide

// DON'T use timestamps (can collide in high-traffic scenarios)
const idempotencyKey = Date.now().toString(); // ❌ Not unique enough

// DON'T use random values (can't retry with same key)
const idempotencyKey = Math.random().toString(); // ❌ Non-deterministic

Usage Scenarios

Problem: Network Error

Without idempotency:

1. Client sends request → Timeout (no response)
2. Client retries → Creates second invoice ❌
3. Result: Duplicate invoices, angry customer

With idempotency:

1. Client sends request with key "abc123" → Timeout
2. Client retries with same key "abc123" → API detects duplicate
3. Result: Returns original invoice ✅

Problem: Automatic Retry Logic

// ❌ Without idempotency - DANGER
async function createInvoice(data) {
  try {
    return await api.post('/v1/invoices', data);
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      return await api.post('/v1/invoices', data); // Creates duplicate!
    }
  }
}

// ✅ With idempotency - SAFE
async function createInvoice(data, idempotencyKey) {
  const headers = { 
    'Authorization': `Bearer ${process.env.BEEL_API_KEY}`,
    'Idempotency-Key': idempotencyKey 
  };

  try {
    return await api.post('/v1/invoices', data, { headers });
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      // Same key ensures we get the original response
      return await api.post('/v1/invoices', data, { headers }); // ✅ Safe
    }
  }
}

Problem: Concurrent Requests

// ✅ Multiple systems creating the same logical invoice
// (e.g., Shopify + manual ERP sync)

// System A
await createInvoice(invoiceData, `shopify-order-${orderId}`);

// System B (runs concurrently)
await createInvoice(invoiceData, `shopify-order-${orderId}`);

// Result: Only ONE invoice is created ✅
// Second request returns the first invoice

Lifetime

Idempotency keys are stored for 24 hours. After this time:

  • The key can be reused for a new operation
  • Attempting to retry with the same key will create a new resource

24-hour limit: If you need to retry a failed operation after 24 hours, generate a new idempotency key.

Response Behavior

First Request (successful)

HTTP/1.1 201 Created
Content-Type: application/json
Idempotency-Replay: false

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "invoice_number": "FAC-2025-0001",
    "status": "DRAFT",
    ...
  }
}

Duplicate Request (with same key)

HTTP/1.1 200 OK
Content-Type: application/json
Idempotency-Replay: true

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "invoice_number": "FAC-2025-0001",
    "status": "DRAFT",
    ...
  }
}

Note: The Idempotency-Replay: true header indicates the response comes from a previous request. The status code may be 200 OK instead of 201 Created.

Error Handling

If the original request failed, retrying with the same key will:

  • Return the original error (if it was a 4xx client error)
  • Retry the operation (if it was a 5xx server error or timeout)
// First request fails with validation error
try {
  await createInvoice(invalidData, 'key-123');
} catch (error) {
  // 422 Unprocessable Entity
  console.log(error.status); // 422
}

// Retrying with same key returns the SAME error
try {
  await createInvoice(invalidData, 'key-123');
} catch (error) {
  // Same 422 error (from cache)
  console.log(error.status); // 422
  console.log(error.headers['idempotency-replay']); // 'true'
}

To fix the error, you must either:

  1. Wait 24 hours for the key to expire
  2. Use a different key (recommended)

Limitations

AspectLimit
Lifetime24 hours
Max length255 characters
Applies toPOST and PUT requests only
Does NOT apply toGET, DELETE (naturally idempotent)
AvailabilityDeveloper Plan only

Official SDK Support

Our official SDKs (coming soon) will auto-generate idempotency keys on every creation request. You won't need to do anything — duplicate protection will work out of the box.

Automatic (default behavior)

Every create() call automatically generates a UUID idempotency key:

// Node.js — idempotency key auto-generated (crypto.randomUUID())
const invoice = await beel.invoices.create(invoiceData);
// Java — idempotency key auto-generated (UUID.randomUUID())
Invoice invoice = beel.invoices().create(invoiceData);
# Python — idempotency key auto-generated (uuid.uuid4())
invoice = beel.invoices.create(invoice_data)

Custom key (optional override)

If you want to use your own key (e.g., to safely retry from your system), pass it as a second argument:

// Node.js — use your own key
const invoice = await beel.invoices.create(invoiceData, `order-${orderId}`);
// Java — use your own key
Invoice invoice = beel.invoices().create(invoiceData, UUID.fromString(myKey));
# Python — use your own key
invoice = beel.invoices.create(invoice_data, idempotency_key=f"order-{order_id}")

Which methods support idempotency?

MethodJavaTypeScriptPython
invoices.create()
invoices.createCorrective()
products.create()
products.createBulk()

All of these auto-generate a UUID if you don't provide one.

Best Practices Summary

Do:

  • Use UUIDs or hash-based keys for uniqueness
  • Include idempotency keys in ALL POST/PUT requests
  • Store the key alongside the request in your database
  • Retry with the SAME key on network errors
  • Use descriptive key names for debugging (e.g., invoice-shopify-order-${id})

Don't:

  • Use generic or repeated values ('create-invoice')
  • Generate random keys that can't be reproduced
  • Rely solely on timestamps (can collide)
  • Forget to implement retries with the same key
  • Use idempotency keys longer than 255 characters

Real-World Example

Complete example with proper error handling:

import { v4 as uuidv4 } from 'uuid';

async function createInvoiceWithRetry(invoiceData: InvoiceData, maxRetries = 3) {
  const idempotencyKey = uuidv4();
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      const response = await fetch('https://app.beel.es/api/v1/invoices', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.BEEL_API_KEY}`,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(invoiceData)
      });

      if (!response.ok) {
        // Client errors (4xx) - don't retry
        if (response.status >= 400 && response.status < 500) {
          throw new Error(`Validation error: ${await response.text()}`);
        }
        // Server errors (5xx) - retry
        throw new Error(`Server error: ${response.status}`);
      }

      const result = await response.json();
      const isReplay = response.headers.get('idempotency-replay') === 'true';
      
      console.log(isReplay ? 'Returned cached invoice' : 'Created new invoice');
      return result.data;

    } catch (error) {
      attempt++;
      if (attempt >= maxRetries) throw error;
      
      // Exponential backoff
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}

// Usage
try {
  const invoice = await createInvoiceWithRetry({
    type: 'STANDARD',
    issue_date: '2025-01-15',
    recipient: { recipient_type: 'EXISTING', customer_id: customerId },
    lines: [{ description: 'Service', quantity: 1, unit_price: 100 }]
  });
  
  console.log('Invoice created:', invoice.invoice_number);
} catch (error) {
  console.error('Failed to create invoice:', error);
}

Next Steps

Questions? Email us at it@beel.es.