NewThree filters returned the wrong rows
BeeL
Get StartedMulti-NIFVeriFactuStripeAPI ReferenceChangelog

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 a POST request, 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 and adds Idempotency-Replay: true
  3. Does not execute the operation again

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

The header name is Idempotency-Key — exactly that. Any other spelling (X-Idempotency-Key, idempotency_key, …) is not recognised and is dropped silently: the request succeeds, no protection is applied, and a retry creates a second invoice.

The key is scoped per user and per environment (a sandbox key never collides with a production one) and is bound to the request body and the target path, so a replay only happens for the exact same operation.

Idempotency-Key is not "one invoice per order". It protects against retries of the same request (network timeouts, double-submits) for 24 hours — it identifies the request, not your business object, and it deliberately keeps returning the original response even if you later delete the created invoice. If what you want is "at most one live invoice per order, and let me recreate it after deleting", that's a business key — use external_ref, not the idempotency key.

POST Requests (creation)

Always use Idempotency-Key when creating resources:

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

Updates (PUT / PATCH)

Updates need no key: they target an existing resource, so repeating one converges on the same state instead of creating a second resource. The API only reads Idempotency-Key on POST — sending it on PUT, PATCH or DELETE has no effect.

Use PATCH to change a few fields; PUT replaces the resource and clears the optional fields you omit:

curl -X PATCH "https://app.beel.es/api/v1/companies/{company_id}/customers/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer beel_sk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "address": {
      "street": "New address 123",
      "number": "45",
      "postal_code": "28001",
      "city": "Madrid",
      "province": "Madrid",
      "country": "España"
    }
  }'

API plan exclusive: Idempotency support is available only with the API plan. See Pricing for details.

Key Generation

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

Format rules — a key that breaks them is rejected with 400 INVALID_IDEMPOTENCY_KEY:

  • Letters, digits, - and _ only (a UUID qualifies, but is not required)
  • Maximum 255 characters
  • Non-empty

✅ Best Practices

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

// Deterministic for ONE attempt: stable so a network retry of that attempt reuses it.
// A new attempt (e.g. after you deleted the previous invoice) needs a NEW key.
const idempotencyKey = `invoice-order-${orderId}-${attemptId}`;

// Want "one invoice per order" as a durable rule? That's `external_ref`
// (see below), not the idempotency key.

// 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/companies/{company_id}/invoices', data);
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      return await api.post('/v1/companies/{company_id}/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/companies/{company_id}/invoices', data, { headers });
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      // Same key ensures we get the original response
      return await api.post('/v1/companies/{company_id}/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

This dedupes concurrent retries within the 24-hour window. For a durable "one invoice per order" guarantee that persists across days and reflects deletions, also set external_ref on the 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.

One invoice per order

Idempotency-Key protects requests; external_ref protects your business object. Pass your own order / cart / contract id when creating an invoice:

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

BeeL. then enforces at most one live standard/simplified invoice per external_ref, unique per issuer:

  • A second create with the same external_ref returns 409 INVOICE_DUPLICATE_EXTERNAL_REFERENCE.
  • Delete that invoice and you can recreate it with the same reference — unlike the idempotency key, this reflects the current state.
  • The rule covers live standard and simplified invoices; other document types are not constrained by it.
  • Fetch the existing invoice anytime: GET /v1/companies/{company_id}/invoices?external_ref=ORD-2025-0042.

The field used to be called external_reference. The old name is still accepted as an alias — on the request body and on the deprecated flat route GET /v1/invoices?external_reference=… — but it will be withdrawn in a future major version. Send external_ref, and filter on the company route above.

Which do I use? Both, for different jobs. Generate a fresh Idempotency-Key (UUID) per creation attempt so a network retry can't duplicate. Use external_ref to tie the invoice to your order and let BeeL. reject business duplicates. Don't encode your order id in the idempotency key to get "one per order" — that's exactly what external_ref is for.

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: Idempotency-Replay: true is the only signal that tells "I created this now" apart from "here is the one from before" — the body is identical in both cases. The status code of a replay may be 200 OK instead of 201 Created, so branch on the header, not on the status.

Idempotency errors

Three responses come from the idempotency layer itself, before your operation runs:

StatusCodeWhenWhat to do
400INVALID_IDEMPOTENCY_KEYThe key breaks the format rules aboveFix the key
409IDEMPOTENCY_KEY_PROCESSINGThe first request with this key is still in flight (concurrent retry)Wait 1–5 s and retry with the same key
409IDEMPOTENCY_KEY_MISMATCHThe key was already used with a different body or a different pathUse a new key — the key identifies one operation, not one endpoint

Error Handling

Failed operations are not stored. Only a successful (2xx) response is cached against the key: if the operation returns 4xx/5xx or blows up, the key is released immediately, so a retry with the same key runs the operation again instead of replaying the failure.

// First request fails with a validation error
try {
  await createInvoice(invalidData, 'key-123');
} catch (error) {
  console.log(error.status); // 422 — nothing was stored under 'key-123'
}

// Fix the payload and retry. Use a NEW key: the body changed, and a body
// that differs from the first one is what triggers IDEMPOTENCY_KEY_MISMATCH
// if the original had succeeded.
await createInvoice(correctedData, 'key-124');

Rule of thumb: same body → same key (safe retry), changed body → new key.

Limitations

AspectLimit
Lifetime24 hours
Max length255 characters
Allowed charactersLetters, digits, -, _
Applies toPOST requests only
Does NOT apply toGET, PUT, PATCH, DELETE (naturally idempotent — the header is ignored)
ScopePer user and per environment (sandbox keys never collide with production ones)
AvailabilityAPI plan only

Official SDK Support

The official Node.js SDK (@beel_es/sdk) auto-generates an idempotency key on every creation request — duplicate protection works 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);

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}`);

Which methods support idempotency?

MethodNode.js / TypeScript
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 an idempotency key in ALL POST requests that create something
  • Spell the header exactly Idempotency-Key
  • 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/companies/{company_id}/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',
    operation_date: '2025-01-15',
    recipient: { 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.