ClientCasa
Integrations

Idempotent writes for safe retries

Every POST to the ClientCasa API should send an Idempotency-Key header. Here's why, how it works under the hood, and how it interacts with payments and refunds.

Networks fail. Servers restart. Your code retries. Without idempotency keys, a retry can create duplicate invoices, duplicate payments, duplicate clients. Every POST and PATCH to the ClientCasa API supports an Idempotency-Key header — and you should always send one.

The 30-second version

Generate a UUID per logical operation. Send it as Idempotency-Key. Retry the exact same request with the exact same key and ClientCasa returns the original response instead of creating a duplicate.

import { ClientCasa } from '@clientcasa/sdk'
import { randomUUID } from 'node:crypto'

const cc = new ClientCasa()
const security = { apiKey: process.env.CLIENTCASA_API_KEY ?? '' }
const idempotencyKey = randomUUID()

await cc.invoices.createInvoice(
  security,
  {
    clientId: 'cli_...',
    issueDate: '2026-05-19',
    dueDate: '2026-06-18',
    lineItems: [{ description: 'Consult', quantity: 1, unitPrice: 500, taxable: false }],
  },
  { headers: { 'Idempotency-Key': idempotencyKey } },
)
curl -s "https://www.clientcasa.com/api/v1/invoices" \
  -X POST \
  -H "x-api-key: $CLIENTCASA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @invoice.json

How it works under the hood

When your request includes an Idempotency-Key:

  1. ClientCasa computes a content hash of method + path + body
  2. Looks up (your_org_id, idempotency_key) in the idempotency store
  3. Cache hit + matching hash: returns the cached response. No DB write happens.
  4. Cache hit + DIFFERENT hash: returns 409 conflict — you tried to reuse a key with different data
  5. Cache miss: proceeds normally, caches the response under that key

Cached responses live for 24 hours. After that the key is eligible for GC and reuse with new data.

When to generate a fresh key

SituationSame key?
Network timeout, retry the SAME request✅ same
5xx error from ClientCasa, retry✅ same
429 rate-limited, retry after Retry-After✅ same
User clicked "Submit" again on the form✅ same (or skip the second submit entirely)
Creating a different invoice❌ new key
Retrying after fixing a 400 validation error❌ new key (your data is different now)

Where this matters most: payments

The Payments resource is immutable after creation — you can't delete one, and the only fields you can update are reference and notes. That means a duplicate POST creates a duplicate money event, which is a real reconciliation mess.

// IDEMPOTENCY ESPECIALLY CRITICAL HERE
await cc.payments.createPayment(
  security,
  {
    clientId: 'cli_...',
    amount: 1250.00,
    receivedDate: '2026-05-19',
    method: 'check',
    reference: 'Check #4421',
  },
  { headers: { 'Idempotency-Key': paymentEntryUUID } },
)

Best practice: persist the idempotency key alongside your business record before the API call. If your process crashes and retries from scratch, it finds the existing key in your DB and reuses it.

// pseudocode
const key = await db.getOrCreateIdempotencyKey('record-' + recordId)
await cc.payments.createPayment(security, paymentInput, {
  headers: { 'Idempotency-Key': key },
})

Refunds: the same pattern

Refunds are recorded as negative-amount Payments with kind: 'refund' and refundOfId referencing the original payment. They need idempotency too:

await cc.payments.createPayment(
  security,
  {
    clientId: 'cli_...',
    amount: -250.00,           // negative
    receivedDate: '2026-05-19',
    method: 'check',
    kind: 'refund',
    refundOfId: 'pay_abc123',  // the original payment
  },
  { headers: { 'Idempotency-Key': refundUUID } },
)

A duplicate refund without idempotency = your customer gets paid twice.

Conflict responses

If you send the same key with a different body, you get a 409:

{
  "error": {
    "code": "conflict",
    "message": "Idempotency key reused with different payload",
    "requestId": "req_abc..."
  }
}

This is intentional — it surfaces bugs in your retry logic (e.g., regenerating the body on each attempt but reusing the key). Either:

  • Generate a new key if the body genuinely should be different
  • Pin the body so retries send the exact same data

What doesn't need idempotency

  • GET requests — read-only; safe to retry without a key
  • DELETE requests — idempotent by definition (deleting twice is a no-op). We don't reject duplicate DELETEs
  • PATCH on most resources — generally safe because the operation is "set to this value," but for cases like incrementing counters via PATCH, idempotency keys still help

We accept the Idempotency-Key header on PATCH because some user PATCH operations are not truly idempotent (e.g., status transitions that fire webhooks).

Key naming and storage

  • Use UUID v4 (built-in: crypto.randomUUID() in modern Node and browsers)
  • Keys are scoped to your organization — the same key in two orgs is unrelated
  • Store the key in your DB before the API call, not after — survives crashes mid-request
  • Don't reuse keys for unrelated operations — collisions return 409

See also

On this page