Error taxonomy

4xx are problems you can fix. 5xx are ours. Every error body has the shape { "error": "<machine_code>", "message": "<human prose>", … }.

4xx — tenant-actionable

CodeerrorMeaning & what to do
400(framework message)Malformed JSON body. There is no stable bad_request code. Fix your serialization.
401missing_tenant_headerX-GFN-Tenant absent.
401unknown_tenantTenant id not recognized.
401tenant_inactiveTenant exists but is not active — contact ops.
401hmac_verify_failedSignature/timestamp failed. reason ∈ { missing_headers, stale_timestamp, future_timestamp, bad_signature }. Check clock skew + which secret. See Auth.
402insufficient_creditsBalance < required hold. Body: required. Top up (fiat). The post-check balance field is deferred post-beta.
403job_type_not_allowedjobType not in your allowed_job_types. Body: jobType, allowed. An authorization / product-contract denial. 451 is reserved for a future counsel-approved legal block and is NOT used here.
404not_foundSubmission id not found for this tenant. Cross-tenant reads also return 404 (existence is never leaked).
409terminal_stateAction invalid for a terminal submission (e.g. cancel on a settled one). Body: status.
409idempotency_key_conflictKey reused with a different body. Body: original submissionId. Mint a fresh key for the different job. See Idempotency.
422validation_errorRequest/payload failed schema validation. Body: details. Variants: unknown_job_type, unknown_tier, quote_failed.
429rate_limitedPer-tenant dispatch rate limit exceeded. Body: retryAfterSeconds. Back off.
409solana_disabledOnly on POST /v1/credits/purchase-intent — the inbound Solana Pay continuity rail is off for this deployment. Use the fiat rail.

5xx — server-side

CodeerrorMeaning
500internal_errorUnhandled exception. Retry idempotent calls; report persistent 500s.
503dispatch_unavailableNo operator could accept at dispatch time; the credit hold is released. Body: submissionId. Retry later.
503not_ready/ready only: database unreachable.

Typed SDK errors

The SDK maps each non-2xx response to a typed error; all extend GoFundNodeApiError so you can catch broadly.

GoFundNodeApiError                       ← base { status, code, requestId, body }
├── GoFundNodeAuthError                  ← 401
├── GoFundNodeInsufficientCreditsError   ← 402 (carries required + available)
├── GoFundNodeIdempotencyConflictError   ← 409 idempotency conflict
├── GoFundNodeValidationError            ← 422 (carries errors[])
├── GoFundNodeRateLimitError             ← 429 (carries retryAfterMs)
├── GoFundNodeServerError                ← 5xx
└── GoFundNodePaymentIntentError         ← purchase intent expired/failed

GoFundNodeNetworkError                   ← transport (DNS, ECONNREFUSED, …)
└── GoFundNodeTimeoutError               ← request aborted by timeout
import {
  GoFundNodeInsufficientCreditsError,
  GoFundNodeIdempotencyConflictError,
  GoFundNodeApiError,
} from "@gofundnode/vendor-client";

try {
  await client.submitAtsApplication({ /* … */ });
} catch (err) {
  if (err instanceof GoFundNodeInsufficientCreditsError) {
    topUp(err.required ?? 0);                  // fiat top-up
  } else if (err instanceof GoFundNodeIdempotencyConflictError) {
    throw err;                                 // caller bug — reused key, different body
  } else if (err instanceof GoFundNodeApiError) {
    log.warn({ status: err.status, requestId: err.requestId }, "gfn api error");
    throw err;
  } else {
    throw err;                                 // network / programming error
  }
}
Reference: docs/V1-API.md §6 (in the repository). The SDK error tree is in the @gofundnode/vendor-client README §5.
← Webhooks Credits & account snapshot →