Skip to main content

Developers · Guide

Idempotency

Make every write safe to retry with an Idempotency-Key, and update a record at the version you read with If-Match.

A connection can drop after the API has made a change but before your system hears the answer. An idempotency key lets your system send the request again without making the change twice.

Send a key with every write

Every request that creates or changes something (POST, PUT or PATCH) must carry an Idempotency-Key header: a value your system makes up, 1 to 255 characters, unique to that one change. A UUID is a good key. A write with no key, an empty one or a longer one answers 400 idempotency_key_required and changes nothing.

A write with its key
curl -X POST "https://your-org.backed.giving/api/v2/funding_applications/1287/notes" \
  -H "Authorization: Bearer $BACKED_TOKEN" \
  -H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13" \
  -H "Content-Type: application/json" \
  -d '{"body":"Budget checked against the supplier quote."}'

What a retry gets

What the API does with a key
You sendThe API answers
The same key, method, path and body within 24 hoursThe first request’s status and body again, with Idempotent-Replayed: true. Nothing is written a second time.
The same key with a different method, path or body422 idempotency_key_reused. Nothing is written.
The same key while the first request is still running409 idempotency_in_progress with Retry-After. Nothing is written: retry after that many seconds.
  • The path includes its query string. The body is compared as JSON, so the order of its properties does not matter.
  • A key belongs to the credential that sent it: two credentials never share one.
  • A refused write keeps nothing against its key, so you can send the same key again once you have fixed the request.
  • After 24 hours the key is forgotten, and the same key is a new request.

If a key keeps answering 409 idempotency_in_progress, its first request stopped part-way. Check whether the record exists before you write again with a new key.

Retrying a write safely (Node.js)
import { randomUUID } from 'node:crypto';

async function write(method, path, body, token) {
  const key = randomUUID(); // one key per change, reused on every retry of it
  for (let attempt = 1; ; attempt += 1) {
    const response = await fetch(`https://your-org.backed.giving${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${token}`,
        'Idempotency-Key': key,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    }).catch(() => null); // a dropped connection: retry with the same key
    const retryable = response === null || [409, 429, 500, 503].includes(response.status);
    if (!retryable || attempt === 5) return response;
    const wait = Number(response?.headers.get('Retry-After') ?? 2 ** attempt);
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

If-Match: write at the version you read

A PATCH that updates a record must carry an If-Match header holding the record’s lock_version as your system last read it, bare or quoted (4 or "4"). If someone changed the record since, the update answers 412 precondition_failed and nothing changes: read the record again, then retry with its new lock_version and a new key.

curl -X PATCH "https://your-org.backed.giving/api/v2/registration_entities/311" \
  -H "Authorization: Bearer $BACKED_TOKEN" \
  -H "Idempotency-Key: 0b5e6f1a-93d2-4c47-8a1e-5f2d7c9b3e60" \
  -H "If-Match: 4" \
  -H "Content-Type: application/json" \
  -d '{"founded_on":"2014-03-01"}'