Auth & HMAC signing
Every authenticated request is signed with HMAC-SHA256. The SDK does this for you; this page is for when you sign by hand or debug a 401.
Headers (required on every authenticated request)
| Header | Value |
|---|---|
X-GFN-Tenant | Your tenant public id (cleartext). |
X-GFN-Timestamp | Unix seconds (integer, UTC). |
X-GFN-Signature | hex(hmac_sha256(secret, timestamp + "." + raw_body)) |
The canonical string
The string you sign is exactly:
<timestamp>.<raw_body>
Sign the body byte-for-byte as transmitted, including whitespace and key order. If you serialize, sign, then re-serialize (or let a framework re-encode the JSON), the bytes change and the signature fails 100% of the time. GET requests sign an empty body.
Sign it (Node, no SDK)
import { createHmac } from "node:crypto";
function signedHeaders(tenantApiId, hmacSecret, rawBody) {
const ts = Math.floor(Date.now() / 1000).toString();
const sig = createHmac("sha256", hmacSecret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return {
"X-GFN-Tenant": tenantApiId,
"X-GFN-Timestamp": ts,
"X-GFN-Signature": sig,
"Content-Type": "application/json",
};
}
const rawBody = JSON.stringify(body); // serialize ONCE
const res = await fetch(`${baseUrl}/v1/submissions`, {
method: "POST",
headers: signedHeaders(tenantApiId, hmacSecret, rawBody),
body: rawBody, // send the SAME bytes you signed
});
Sign it (shell)
TS=$(date +%s)
BODY='{"jobType":"ats.application","tier":"standard","idempotencyKey":"af-1","payload":{}}'
SIG=$(printf '%s' "${TS}.${BODY}" | openssl dgst -sha256 -hmac "$GFN_HMAC_SECRET" -r | cut -d' ' -f1)
curl -X POST "$GFN_API_BASE_URL/v1/submissions" \
-H "X-GFN-Tenant: $GFN_TENANT_API_ID" \
-H "X-GFN-Timestamp: $TS" \
-H "X-GFN-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-binary "$BODY"
Replay window
The timestamp must be within ±5 minutes (300s) of server time. Keep your clock in sync (NTP). A skewed clock is the most common cause of intermittent 401s.
Two separate secrets
Inbound request signing and outbound webhook verification use different secrets, by design — compromise of one direction does not enable the other.
| Secret | Used for |
|---|---|
| HMAC secret | Signing your requests TO GoFundNode (this page). |
| Webhook secret | Verifying webhooks FROM GoFundNode. See Webhooks. |
Mixing them up causes every request (or every webhook verification) to 401.
401 failure reasons
Auth failures return 401. The body tells you why:
error | Meaning |
|---|---|
missing_tenant_header | X-GFN-Tenant absent. |
unknown_tenant | Tenant id does not match a tenant. |
tenant_inactive | Tenant exists but is not active. |
hmac_verify_failed | Signature/timestamp check failed. Body reason ∈ { missing_headers, stale_timestamp, future_timestamp, bad_signature }. |
Reference:
docs/V1-API.md §1 (auth model) and §6.1 (error taxonomy), in the repository.