Docs

Idempotency

POST endpoints accept an Idempotency-Key header so you can safely retry on network failure without creating duplicate effects.

Set Idempotency-Key on any state-mutating request to a UUID or other random opaque string (length 1–255):

POST /v1/partners/.../...
Idempotency-Key: 6f8e2c44-a13e-4c8f-9d8b-1c2d3e4f5a6b
Authorization: Bearer tk_live_...
Content-Type: application/json

{ "field": "value" }

Behavior

  • On first execution, Tendral runs the request and stores (status, headers, body) for 24 hours, keyed on (api_key_id, idempotency_key).
  • A retry within the 24-hour window with the same key + same request body returns the original response with header Idempotent-Replayed: true.
  • A retry with the same key + different request body returns 409 IDEMPOTENCY_KEY_REUSED (error.type = idempotency_error). The original effect is preserved; the reuse attempt is rejected.
  • A retry with a fresh key creates a brand-new effect. Always use a fresh key for any new operation.

Recommended pattern

Generate a UUID v4 client-side, retry on network failure or 5xx with exponential backoff, and only mark the operation done after a 2xx (whether or not Idempotent-Replayed is set):

import { randomUUID } from 'node:crypto'

async function callWithRetry(body) {
  const key = randomUUID()
  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await fetch('https://api.tendralhealth.com/v1/partners/.../...', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.TENDRAL_API_KEY}`,
        'Idempotency-Key': key,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
    if (r.ok) return r.json()
    if (r.status >= 400 && r.status < 500) throw new Error(await r.text()) // permanent
    await sleep(Math.min(60_000, 1000 * 2 ** attempt) * (0.8 + Math.random() * 0.4))
  }
  throw new Error('Exceeded retry budget')
}

Caveats

  • Keys expire after 24 hours. After that, the same key + same body creates a new effect — the dedup record has been swept.
  • Keys are scoped per API token. Two different bearer tokens with the same Idempotency-Key are independent.
  • Don't reuse keys across different operations. The body-hash check catches accidental reuse; intentional reuse will fail with IDEMPOTENCY_KEY_REUSED.