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)

HeaderValue
X-GFN-TenantYour tenant public id (cleartext).
X-GFN-TimestampUnix seconds (integer, UTC).
X-GFN-Signaturehex(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.

SecretUsed for
HMAC secretSigning your requests TO GoFundNode (this page).
Webhook secretVerifying 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:

errorMeaning
missing_tenant_headerX-GFN-Tenant absent.
unknown_tenantTenant id does not match a tenant.
tenant_inactiveTenant exists but is not active.
hmac_verify_failedSignature/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.
← Quickstart Idempotency & retries →