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
| Code | error | Meaning & what to do |
|---|---|---|
| 400 | (framework message) | Malformed JSON body. There is no stable bad_request code. Fix your serialization. |
| 401 | missing_tenant_header | X-GFN-Tenant absent. |
| 401 | unknown_tenant | Tenant id not recognized. |
| 401 | tenant_inactive | Tenant exists but is not active — contact ops. |
| 401 | hmac_verify_failed | Signature/timestamp failed. reason ∈ { missing_headers, stale_timestamp, future_timestamp, bad_signature }. Check clock skew + which secret. See Auth. |
| 402 | insufficient_credits | Balance < required hold. Body: required. Top up (fiat). The post-check balance field is deferred post-beta. |
| 403 | job_type_not_allowed | jobType 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. |
| 404 | not_found | Submission id not found for this tenant. Cross-tenant reads also return 404 (existence is never leaked). |
| 409 | terminal_state | Action invalid for a terminal submission (e.g. cancel on a settled one). Body: status. |
| 409 | idempotency_key_conflict | Key reused with a different body. Body: original submissionId. Mint a fresh key for the different job. See Idempotency. |
| 422 | validation_error | Request/payload failed schema validation. Body: details. Variants: unknown_job_type, unknown_tier, quote_failed. |
| 429 | rate_limited | Per-tenant dispatch rate limit exceeded. Body: retryAfterSeconds. Back off. |
| 409 | solana_disabled | Only on POST /v1/credits/purchase-intent — the inbound Solana Pay continuity rail is off for this deployment. Use the fiat rail. |
5xx — server-side
| Code | error | Meaning |
|---|---|---|
| 500 | internal_error | Unhandled exception. Retry idempotent calls; report persistent 500s. |
| 503 | dispatch_unavailable | No operator could accept at dispatch time; the credit hold is released. Body: submissionId. Retry later. |
| 503 | not_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.