# Webhook verification example in TypeScript

This synthetic example checks a signature with Bun. It uses a teaching format: HMAC-SHA256 over `timestamp`, a dot, and the unchanged body bytes. It contains no real secret or customer data.

## Download and run

Install Bun from https://bun.sh/docs/installation. Save these three files in one folder:

- [verify.ts](./verify.ts): signer and verifier.
- [verify.test.ts](./verify.test.ts): valid, altered, stale, and malformed-input cases.
- [LICENSE](./LICENSE): MIT license. Keep its notice when reusing the code.

From that folder, run:

```sh
bun test verify.test.ts
```

No package install is needed. The current suite has ten tests.

## Inputs and time rule

`verify(secret, timestamp, rawBody, claimedHex, now)` returns a boolean. The body is a string or `Uint8Array`, including a Node `Buffer`. Pass the original request bytes. Parsing and serializing JSON before this check changes the signed message.

Both times are non-negative whole Unix seconds. For live time, use `Math.floor(Date.now() / 1000)`. This demo allows a timestamp within 300 seconds on either side of `now`, including the boundary. A missing time, `NaN`, a fraction, empty secret, invalid body type, or malformed signature returns `false`. `sign` throws for invalid signing inputs so a bad fixture is visible.

Choose the endpoint secret from trusted server configuration. An empty secret is rejected; that check does not prove a configured secret is correct or strong. The test value `demo_secret` is public and must never protect a real endpoint.

## Connect a provider

This code does not parse Stripe or Paystack headers. Providers have different signature formats, algorithms, rotation rules, and time checks. Use the provider's SDK and current documentation for a real integration. In Stripe, pass the exact body, `Stripe-Signature` header, and endpoint secret to its SDK verifier.

Verification alone does not prevent valid redelivery. After verification, validate the event's account and operation IDs. Store the event ID under a unique database constraint in the same transaction as the ledger effect. For a queue, use durable inbox storage and record processing state; a queue push and a database write are separate failure points.

The tests cover signature checks, exact bytes, invalid clocks, and time boundaries. They do not test a provider header, secret rotation, queue, database transaction, or final payment state.

Sources:

- https://docs.stripe.com/webhooks/signature
- https://docs.stripe.com/webhooks
