Handshakes
Most providers won't deliver anything until you answer a one-time challenge — and the right order differs.
Most providers will not deliver anything until you answer a one-time challenge. This is the part most webhook code forgets — and it’s why a freshly deployed endpoint that “never receives events” is usually an endpoint that never answered its setup probe.
The SDK answers handshakes for you. A handshake short-circuits before
dispatch and reports outcome: 'handshake', so your event handlers never
see it.
Two classes, and the order matters
Unsigned challenges — Meta’s hub.challenge, Asana’s X-Hook-Secret —
must be answered before verification, because the request is often what
establishes the secret. There is nothing to verify against yet.
Signed challenges must be verified first. Answering them before
verifying is a security hole — and sometimes a setup failure: Discord probes
a new endpoint with a deliberately invalid signature and refuses to save the
URL unless it receives a 401.
Providers declare which class they are, and the handler orders the two calls to match. You don’t configure this; it’s part of each provider’s definition.
What the shipped providers expect
| Provider | Trigger | Expected answer | Signed? |
|---|---|---|---|
| Discord (interactions) | POST with type: 1 (PING) |
{ "type": 1 } |
✅ must reject a bad signature with 401 |
| Discord (webhook events) | POST with type: 0 (PING) |
bare 204 |
✅ same |
| Google Pub/Sub | none — but the endpoint must return 2xx fast | ack within the deadline or it redelivers | n/a |
| Stripe, GitHub, Twilio, Standard Webhooks | none | — | — |
Observing handshakes
If you process requests manually, the result tells you when a handshake was answered:
const result = await handler.process(request)
if (result.outcome === 'handshake') {
// setup probe answered; no event was dispatched
}