Skip to main content

Key lifecycle & security

What you hold​

CredentialFormatSensitivity
Key IDpk_<org>_test_<hex> (sandbox) / pk_<org>_live_<hex> (production)Identifies you; safe to log
HMAC secretOpaque string, shown once when the key is issuedProves it is you. Signs every request you send and verifies every webhook we send

Each environment has its own key. Nothing carries over from sandbox to production — the live key, callback URL, payment artefacts and IP allowlist are all configured again at go-live (onboarding).

Storage rules​

  • The HMAC secret goes in your secrets manager only — never in code, config repos, logs, or client-side apps. It is shown exactly once at issuance; if you lose it, request a rotation.
  • The key ID is public-safe (it identifies; the signature proves).
  • Server-side only: signing must never happen in a browser or mobile app you ship to end users.
  • Your webhook receiver needs the same secret. Store it once, reference it from both places, and make the receiver's verifier accept a list of secrets (see rotation below).

Rotation (zero-downtime)​

Rotate for scheduled hygiene or on suspected exposure. The exact semantics matter, so here they are in order:

  1. Before you ask for a rotation, make sure your webhook receiver can verify a delivery against more than one secret (try the current secret, then the new one). This is the step teams skip, and it is the only step that can cost you deliveries.
  2. Request the rotation (write to [email protected]). We issue a new secret, shown once.
  3. From that moment the new secret is the primary one: webhooks are signed with the new secret immediately. Requests you send are accepted with either the old or the new secret.
  4. Deploy the new secret to your signer at your own pace, then confirm.
  5. We retire the old secret. Requests still signed with it now fail as 401 SIGNATURE_INVALID — and count toward the lockout below — so finish step 4 before you confirm.

If step 1 is done, downtime is zero. If it is not, every webhook delivered between step 3 and your receiver picking up the new secret will fail verification on your side — they are retried on the normal ladder and remain replayable, so nothing is lost, but you will be chasing a backlog.

Receiver: verify against every secret you currently hold
const crypto = require("node:crypto");
const SECRETS = [process.env.FP_SECRET_CURRENT, process.env.FP_SECRET_NEXT].filter(Boolean);

/**
* callbackPath — your callback URL's pathname only (no query string)
* ts — the X-Fieldproof-Timestamp header value, verbatim
* rawBody — the request body exactly as received (string or Buffer), NOT a re-serialised object
* receivedSig — the X-Fieldproof-Signature header value
*/
function verify(callbackPath, ts, rawBody, receivedSig) {
const canonical = Buffer.concat([Buffer.from(`POST\n${callbackPath}\n${ts}\n`, "utf8"), Buffer.from(rawBody)]);
const received = Buffer.from(String(receivedSig || "").toLowerCase());
return SECRETS.some((secret) => {
const expected = Buffer.from(crypto.createHmac("sha256", secret).update(canonical).digest("hex"));
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
});
}

Revocation​

If a key is compromised, revocation takes effect within 60 seconds of the request (that is the key cache TTL). Every call with that key then fails with 401 UNAUTHORIZED, so your integration halts until a replacement is issued. Prefer rotation unless the secret is definitely exposed.

Automatic protections on our side​

ProtectionBehaviour
Signature-failure lockout10 invalid signatures within 300 s → the key is locked for 900 s. Every request during the lock — including correctly signed ones — gets 429 AUTH_LOCKED with Retry-After set to the seconds remaining. A leaked key ID without its secret is useless and self-limiting.
Environment lockingTest keys are rejected on production and live keys on the sandbox (401 UNAUTHORIZED) — a misconfiguration fails loudly, immediately.
Replay windowTimestamps more than 5 minutes from server time are rejected (401 TIMESTAMP_SKEW).
IP allowlist (optional)Set at onboarding, per environment. Entries are plain IPv4 addresses or CIDR blocks. We resolve your source address from CF-Connecting-IP (or the last hop of X-Forwarded-For); a request from anywhere else is 401 UNAUTHORIZED.
Rate limiting60 requests/min per key (429 RATE_LIMITED, Retry-After set); POST /cases additionally 10 batches/hour, dry-runs and idempotent replays included.
Request tracingEvery response carries X-Request-Id. Quote it when you contact [email protected].

:::warning We do not page your on-call The platform does not send alerts to your technical contact — not for a lockout, not for exhausted webhook deliveries, not for a tripped circuit breaker. Our operations team is alerted internally. On your side, treat 429 AUTH_LOCKED in your client as an incident (stop and back off for Retry-After seconds — a correct signature does not unlock early), and monitor delivery health yourself with GET /api/v1/partner/webhooks/deliveries and GET /api/v1/partner/health. :::

Allowlisting our traffic on your side​

Your callback URL (and, for the org-gateway payment mode, your mint URL) must be https on a public IP; we do not follow redirects. We do not publish static egress IPs, so do not build a firewall rule that assumes them — verify our webhooks by signature instead. If your security policy requires egress IPs, contact [email protected] before go-live.