Verify deliveries to your app
Check the signature on every delivery with the destination’s signing secret, so your app acts only on webhooks we delivered.
Anyone who knows your destination’s address can send it a request. Every delivery is signed with the destination’s signing secret, so your app can check that it came from us, unchanged, and recently, and refuse everything else.
Deliveries follow the Standard Webhooks way of signing, so any library for it can check them.
Get the signing secret
Each destination has its own signing secret, which starts with whsec_:
curl "$HOOKS_URL/destinations/$DESTINATION_ID/secret" \
-H "Authorization: Bearer $HOOKS_API_KEY"{
"data": { "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" },
"meta": { "requestId": "01m3c081dbf3j55x7hsa7wc1yj" }
}Keep it in your app’s secret settings, such as an environment variable. In the console, open the destination and show its Signing secret.
The signing headers
| Header | Holds |
|---|---|
webhook-id | The event’s ID. The same on every attempt, and when a delivery is sent again |
webhook-timestamp | When this attempt was sent, in seconds since 1 January 1970 |
webhook-signature | v1, and the signature, in base64 |
The signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, with the key being the part of the secret after whsec_, decoded from base64. The body is the body exactly as it was sent, byte for byte.
Check a delivery
Read the raw body
Read the body as bytes or text exactly as it arrived, before any parsing. A body that was parsed and turned back into text does not match.
Check the time
Refuse a delivery whose
webhook-timestampis more than 5 minutes from now, so an old delivery cannot be sent to you again.Check the signature
Work out the signature, and compare it with each one in
webhook-signaturein constant time. Refuse the delivery when none matches.Skip what you have handled
Record each
webhook-idyou handle, and answer200without doing anything when one comes again.
space.hooks.verify() does every check for you:
import { SpaceError, space } from "digitospace"
export async function POST(request: Request): Promise<Response> {
try {
const delivery = await space.hooks.verify(request, {
secret: process.env.WEBHOOK_SECRET ?? "",
})
const payment = delivery.json<{ reference: string; amount: number }>()
console.log(`Order ${payment.reference} paid TZS ${payment.amount}`)
return new Response(null, { status: 200 })
} catch (error) {
if (error instanceof SpaceError && error.code === "invalid_signature") {
return new Response(null, { status: 401 })
}
throw error
}
}Reset the signing secret
If the secret leaks, reset it. Every attempt from then on is signed with the new secret, so update your app straight away:
curl -X POST "$HOOKS_URL/destinations/$DESTINATION_ID/secret" \
-H "Authorization: Bearer $HOOKS_API_KEY"The answer holds the new secret.
When a real delivery fails the check
Look for a secret from another destination, a secret that was reset, a body changed before the check, or a server clock that is more than 5 minutes out. Answer a delivery that fails the check with 401, and it is tried again like any other failed attempt.
Last updated