Skip to content

Idempotency

When you submit a POST request that creates a Transaction, Batch, Subclient, Subclient Account, or other resource, your network may drop the response, your client may time out, or your job runner may retry. Without a guard against duplicates, a retry can create a duplicate, and in a payments API, duplicates move real money you didn't intend to move.

GIB exposes a body-level field, external_key, that you populate with a unique value per logical operation. The API treats external_key as the duplicate-detection key. Submitting the same external_key twice (with the same User and Type) returns 409 ConflictError instead of creating a second resource.

How it works

Include external_key in the request body on any POST that creates a resource:

POST /transaction/from-subledger
{
  "external_key": "TX-2026-05-04-payroll-employee-42",
  "type": "ach",
  ...
}

The value is your choice; UUIDv4 works, but a deterministic string derived from your domain (e.g. payroll-{run_id}-{employee_id}) is often clearer for operational debugging.

First call

GIB processes the request normally and stores the resource with the external_key recorded against the (User, Type) tuple.

Retry, same external_key

A second request with the same external_key (same User, same Type) returns 409 ConflictError. The response includes the X-Duplicate-Transaction-Transfer-Keys header listing the original Transaction(s) so your code can correlate.

HTTP/1.1 409
X-Duplicate-Transaction-Transfer-Keys: 0987654321
Content-Type: application/json

{
  "error": "ConflictError: Duplicate transaction found",
  "exception_class": "ConflictError"
}

Unlike a header-based idempotency system, GIB does not replay the original 200 response. A duplicate external_key always produces a 409. Treat the 409 as confirmation that your earlier request landed.

Worked example: recovering from a dropped response

The pieces above combine into one safe retry sequence. Suppose you submit a transaction and never see the response:

  1. Submit the POST with an external_key. The network drops the response, so your client times out without knowing whether the transaction was created.
  2. Retry the identical request, same external_key, same User and Type.
  3. Read the outcome. A 409 ConflictError means your first request did land, treat it as success, not failure. (A 2xx instead means the first request never landed and this retry created the transaction.)
  4. Correlate. On the 409, read the X-Duplicate-Transaction-Transfer-Keys response header for the original transaction's transfer key(s).
  5. Confirm (optional). Retrieve the original's current state with getTransactions if you need its fields or status.

Either branch leaves you with exactly one transaction and its transfer key, and no duplicate movement of funds.

Retry without external_key

If you didn't provide an external_key, GIB still attempts duplicate detection using a fallback fingerprint of core fields for that resource type. For Transactions this includes fields such as User, Type, Transfer Type, Amount, and routing/account numbers; for other resource types (Subclients, etc.) it matches on fields like name and type.

This fallback covers the case where you forget the external_key, but it's brittle: a legitimate second operation with the same core fields within a short window can also trip the check. Always send external_key for production traffic.

The exact fields used differ per operation and are documented in each operation's description in the API reference.

Overriding a duplicate (transactions only)

If you have a legitimate need to force-submit a transaction that GIB flagged as a duplicate (for example, a genuine re-run of a payment that was already returned), include the X-Duplicate-Override-Reason request header with a plain-text explanation:

POST /transaction/from-subledger
Authorization: <key>
X-Duplicate-Override-Reason: Legitimate re-submission after R01 return on 2026-05-04

GIB will process the request and log the reason against the transaction for audit purposes. The header is optional; omitting it means any detected duplicate still returns 409.

Key generation

A good external_key is:

  • Unique per logical operation. Don't reuse a key for two different transactions, even if they look similar.
  • Stable across retries. A retry of the same operation must use the same key. That's the whole point.
  • Generated client-side. GIB never generates the key; you do.
  • Deterministic when possible. If your handler is processing an event from another system, derive the key from that event's ID. Every retry of your handler computes the same key.

UUIDv4 is the simplest:

import uuid
external_key = str(uuid.uuid4())

For event-driven workflows:

external_key = f"webhook-{event_id}-tx-{tx_index}"

When to use it

Always, on any POST that creates a resource. Specifically:

It costs nothing extra and protects you from a class of bugs that's expensive to debug after the fact.

Idempotency vs. duplicate detection

These are the same mechanism in GIB's API: external_key drives both. Where the field is empty, the API falls back to fingerprint-based duplicate detection on core fields for that resource type. See Security → Duplicate Detection.