Verifying webhooks
HMAC-SHA256 with replay protection. Six lines on your end.
Every webhook Strimz sends is signed. You verify the signature on receipt, reject anything that doesn't match, and your endpoint is safe even if the public URL leaks.
The header
t is the Unix timestamp at signing time. v1 is the lowercase hex of hmac_sha256(secret, "${t}.${rawBody}").
We sign the raw body, byte-for-byte, before any framework parses it. If your framework re-serialises JSON in a slightly different shape (whitespace, key order), the signature won't match. Capture the raw body before parsing.
The receiver
The reference implementation lives in @strimz/sdk:
If you're not using Node, here's the algorithm to translate:
Replay protection
The t timestamp is your defence against an attacker capturing a real webhook and re-sending it later. Reject anything where |now - t| > 300s. We use 5 minutes because clocks drift, but it's tight enough that captured payloads can't sit in someone's logs for an hour and then be replayed.
Idempotency on your end
Strimz uses at-least-once delivery. A flaky transport can cause us to retry a delivery you've already processed. Every delivery has a unique X-Strimz-Delivery-Id header. Persist it; if you've already seen this ID, skip the work and return 200.
Rotating the secret
In the dashboard, Webhooks → endpoint → Rotate secret. You'll get a new secret immediately. The old one stops working the same instant. There's no overlap window. To roll without downtime:
- Update your env to accept either the old or new secret.
- Rotate in the dashboard.
- Wait until you've seen one delivery succeed with the new secret.
- Remove the old secret from your env.
We log every delivery's signature attempts in your dashboard so you can see the cutover happen in real time.
How Strimz stores your secret
You see the signing secret exactly once: in the response to the create-endpoint call (and the same again if you rotate). On the Strimz side, the secret lives in two places, and neither one is plaintext at rest.
Postgres holds an AES-256-GCM ciphertext of the secret. The encryption key sits in an environment variable on our infrastructure, not in the database. A database dump leaks ciphertext and nothing useful.
Redis holds the plaintext as a hot cache for the delivery worker, so we don't pay the decrypt cost on every send. Redis is a cache, not the source of truth. If it's flushed, the scheduler decrypts the column back into the cache on its next boot and webhook delivery keeps going.
We also keep an unsalted sha256(secret) alongside the ciphertext.
It's an integrity index for the rotation flow and cannot be used to
recover the plaintext.
This is the same envelope-encryption shape a hardware KMS would give us. We're using a software-backed symmetric key today because it lets us swap the backend later (HSM, cloud KMS, whatever fits) without re-encrypting any data.
Why HMAC and not RSA / ed25519
HMAC-SHA256 is symmetric, which means there's no public-key infrastructure for you to manage. One shared secret per endpoint, rotatable on demand. That tradeoff matches how most teams already build webhook receivers.
If you want asymmetric signing. So leaking the endpoint's secret can't compromise the signing key itself. open an issue. We'd consider it for the Enterprise plan.
