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.
HMAC-SHA256 (Standard)
Section titled “HMAC-SHA256 (Standard)”Each webhook endpoint has its own signing secret (whsec_…). Phew sends:
Phew-Signature: t=1761220329,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 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:
- Read the raw request body as bytes, before any JSON parsing.
- Compute
HMAC-SHA256(secret, t + "." + body). - Compare with each
v1using a constant-time comparison. - Reject the request if
tis 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); });}import hashlibimport hmacimport time
def verify_phew_signature(raw_body: bytes, header: str, secret: str) -> bool: parts = [part.split("=", 1) for part in header.split(",")] try: timestamp = int(next(value for key, value in parts if key == "t")) except (StopIteration, ValueError): return False if abs(time.time() - timestamp) > 300: return False expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, value) for key, value in parts if key == "v1")function verify_phew_signature(string $rawBody, string $header, string $secret): bool { $timestamp = null; $signatures = []; foreach (explode(',', $header) as $part) { [$key, $value] = array_pad(explode('=', $part, 2), 2, ''); if ($key === 't') $timestamp = (int) $value; if ($key === 'v1') $signatures[] = $value; } if ($timestamp === null || abs(time() - $timestamp) > 300) return false; $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); foreach ($signatures as $signature) { if (hash_equals($expected, $signature)) return true; } return false;}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.1Host: hooks.yourbank.exampleContent-Type: application/jsonContent-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:Phew-Event-Id: evt_01JD9C2E4G6J8L0N2Q4S6U8W0YSignature-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.
Mutual TLS delivery
Section titled “Mutual TLS delivery”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.
Further hardening
Section titled “Further hardening”- 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-Idonce; delivery is at least once, so duplicates are possible.