Webhook Verification
How to verify the authenticity of Passage Connect webhooks.
Overview
Webhook requests include an X-Passage-Signature header containing an ES256-signed JWT. The JWT payload includes a SHA-256 hash of the request body, allowing you to verify both authenticity and integrity.
Step 1: Extract the signature
Read the JWT from the X-Passage-Signature header and the timestamp from X-Passage-Timestamp:
const signature = request.headers.get('X-Passage-Signature')
const timestamp = request.headers.get('X-Passage-Timestamp')Step 2: Decode the JWT header
Parse the JWT header to get the kid (key ID):
const [headerB64] = signature.split('.')
const header = JSON.parse(atob(headerB64))
const kid = header.kidStep 3: Fetch the public key
const res = await fetch('https://connect.services.getpassage.ai/webhook_verification_key/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key_id: kid })
})
const { key } = await res.json() // PEM public keyStep 4: Verify the JWT signature
const publicKey = await crypto.subtle.importKey(
'spki',
pemToArrayBuffer(key),
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['verify']
)
// Verify the ES256 signature over header.payload
const [headerB64, payloadB64, sigB64] = signature.split('.')
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`)
const sig = base64urlDecode(sigB64)
const valid = await crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' },
publicKey,
sig,
data
)Step 5: Verify the body hash
After verifying the JWT signature, check that the body hash matches:
const payload = JSON.parse(atob(payloadB64))
const bodyHash = await sha256Hex(requestBody)
if (payload.request_body_sha256 !== bodyHash) {
throw new Error('Body hash mismatch')
}Security notes
- Always verify webhook signatures before processing
- Cache the public key to avoid fetching on every webhook
- Check the
iatclaim to reject stale webhooks
Next steps
- Webhooks — Webhook overview
- Webhook Key API — Key endpoint reference
Last updated on