Idempotency & retries

A submission is a chargeable side effect. Idempotency keys are how you retry safely without double-charging.

The rule

Pick a stable key

Derive the key from your own durable record — your queue row id, your application id — so the same logical job always produces the same key.

// Good: deterministic, survives a process restart.
const idempotencyKey = `apply-fun:queue:${queueRowId}`;

// Bad: a fresh random key on every attempt creates duplicate submissions.
const idempotencyKey = crypto.randomUUID(); // ← only for one-shot demos

Retry pattern

The SDK deliberately does not auto-retry POST /v1/submissions: a blind retry could double-charge if the first attempt reached the server but the response was lost. Instead, on a transient failure you re-call with the same key — the server dedupes.

import {
  GoFundNodeClient,
  GoFundNodeIdempotencyConflictError,
} from "@gofundnode/vendor-client";

async function submitWithRetry(client, input, maxAttempts = 3) {
  for (let attempt = 1; ; attempt++) {
    try {
      // Same idempotencyKey across every attempt → at-least-once, exactly-once charge.
      return await client.submitAtsApplication(input);
    } catch (err) {
      if (err instanceof GoFundNodeIdempotencyConflictError) {
        // Reused key with a DIFFERENT payload — a caller bug, not transient.
        throw err;
      }
      if (attempt >= maxAttempts) throw err;
      await new Promise((r) => setTimeout(r, 500 * 2 ** attempt)); // backoff
    }
  }
}

Runnable: examples/idempotency-retry.ts in the SDK package demonstrates both the 200 replay and the 409 conflict.

What the SDK retries automatically

Safe-by-default reads + the idempotent cancel are retried with jittered exponential backoff on transport errors, 5xx, and 429:

Not auto-retried: POST /v1/submissions and POST /v1/credits/purchase-intent (both stateful). Use the same-key pattern above.

Cancellation is idempotent too

Cancelling an already-cancelled submission returns the same 200 receipt. Cancelling one that already terminated (submitted / failed) returns 409 terminal_state with the current status. See Submissions.

Webhooks are at-least-once

Webhook delivery is at-least-once; dedupe on the X-GFN-Delivery header before applying any side effect. See Webhooks.

Reference: docs/V1-API.md §7 (idempotency), in the repository.
← Auth & HMAC Submit a submission →