Skip to content

Preview. The API is shared with design partners and may change before launch. Register interest for sandbox access.

Webhook verification

Every webhook Phew sends is signed. Verify the signature before you act on an event, and reject anything that fails.

Each webhook endpoint has its own signing secret (whsec_…). Phew sends:

Phew-Signature: t=1761220329,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is the hex HMAC-SHA256 of {t}.{raw body} with the endpoint secret. During secret rotation, the header contains one v1 per active secret; accept the request if any of them matches.

To verify:

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Compute HMAC-SHA256(secret, t + "." + body).
  3. Compare with each v1 using a constant-time comparison.
  4. Reject the request if t is more than 5 minutes from your clock.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPhewSignature(rawBody: Buffer, header: string, secret: string): boolean {
const parts = header.split(",").map((part) => part.split("=", 2) as [string, string]);
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
return parts
.filter(([key]) => key === "v1")
.some(([, value]) => {
const candidate = Buffer.from(value, "hex");
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
}

Ed25519 with RFC 9421 (Enhanced and Bank-grade)

Section titled “Ed25519 with RFC 9421 (Enhanced and Bank-grade)”

For Enhanced and Bank-grade endpoints, Phew signs webhooks with HTTP Message Signatures, using Ed25519 keys published at https://partner-api.getphew.com/.well-known/jwks.json. There is no shared secret to protect or rotate.

POST /phew/webhooks HTTP/1.1
Host: hooks.yourbank.example
Content-Type: application/json
Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:
Phew-Event-Id: evt_01JD9C2E4G6J8L0N2Q4S6U8W0Y
Signature-Input: phew=("@method" "@target-uri" "content-digest" "phew-event-id");created=1761220329;keyid="phew-wh-2026-10";alg="ed25519"
Signature: phew=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:

To verify, fetch the JWKS (cache it, and refresh when you see an unknown keyid), check that content-digest matches the body, rebuild the signature base and verify it with the key. Reject created values more than 5 minutes old.

Phew publishes a new key at least 7 days before it starts signing with it, so a cached JWKS refreshed daily never misses a rotation.

Bank-grade endpoints can also require mTLS. Phew then presents a client certificate issued for your account when it connects. We send you the CA certificate to trust and the expected subject during onboarding.

  • Only accept webhooks over HTTPS with a valid certificate. Phew won’t deliver to plain HTTP.
  • Optionally allowlist Phew’s webhook source IP ranges, which we publish to partners and announce changes 30 days in advance.
  • Process each Phew-Event-Id once; delivery is at least once, so duplicates are possible.