Courses/Webhook Receiver
Standalone Course~30 min4 challenges

Webhook Receiver

Grit ships a hardened webhook receiver: signature verification for Stripe, GitHub, and generic HMAC providers, automatic deduplication on (provider, external_id), and a replay path so retries are always safe.

Incoming webhookVerify + dispatchYoudispatchPOST /webhooks/:providerStripe · GitHub · HMACVerify signatureHMACStore eventidempotentYour On() handlerruns
Signature checkIdempotent
Signatures verified, events stored idempotently, then your handler runs — replayable from admin

Reference docs: Webhooks →

Verify signatures

Reject forged calls — Stripe, GitHub, and generic HMAC verifiers ship in the box.

Dedup events

A unique (provider, external_id) index makes duplicate deliveries a no-op.

Replay safely

Re-process a stored event without asking the provider to resend it.

Handle the payload

Map a verified event to your domain logic — fulfil orders, sync repos, notify users.

What is a webhook?

A webhook is an HTTP request a third party sends to you when something happens on their side — a Stripe payment succeeds, a GitHub branch is pushed, a Twilio message is delivered. Instead of you polling their API, they push the event.

Why webhooks need hardening: The endpoint is public, so anyone can POST to it. Providers also retry on timeouts, so the same event can arrive several times, and the network can deliver them out of order. A safe receiver must authenticate, deduplicate, and be idempotent.

Verifying the signature

Providers sign each request with a shared secret. Grit recomputes the signature over the raw request body and compares it in constant time. A mismatch is rejected with 401 before any handler runs.

HMAC: Hash-based Message Authentication Code — HMAC(secret, body). Only someone who knows the secret can produce a valid code for a given body, so a matching HMAC proves the request is authentic and unmodified.
conceptual — verification step
// Grit verifies before dispatch; you just register the secret.
// Stripe:  Stripe-Signature header, secret = whsec_...
// GitHub:  X-Hub-Signature-256 header, secret = your webhook secret
// Generic: X-Signature header = hex(hmac_sha256(secret, rawBody))
if !verify(provider, rawBody, signatureHeader, secret) {
    c.JSON(401, gin.H{"error": "invalid signature"})
    return
}
Signatures are computed over the raw bytes. If middleware re-encodes the JSON before verification, the signature won't match — Grit captures the raw body for exactly this reason.

Dedup & replay

Every verified event is stored with the provider's own event id. A unique index on (provider, external_id) means a duplicate delivery is silently ignored — the handler runs exactly once.

Idempotency: Processing the same event N times has the same effect as processing it once. Combined with the unique index, this is what makes provider retries harmless.
the dedup guarantee
// First delivery  -> insert row, run handler
// Retry / duplicate -> insert hits the unique (provider, external_id)
//                      index, is skipped, handler does NOT run again
//
// Replay (manual)  -> re-run the handler against the STORED payload,
//                      no call back to the provider required
Store the raw payload, not just the parsed fields. Replay reads from that stored copy, so you can re-drive a fix through old events without asking Stripe or GitHub to resend.

Practice

1

Challenge: Send a forged request

POST a fake JSON body to your webhook endpoint with no (or a wrong) signature header. What status code do you get? Confirm no handler logic ran.

2

Challenge: Use the Stripe CLI

Run stripe listen --forward-to localhost:8080/api/webhooks/stripe and trigger a test event. Does it verify and process? Find the stored event row in GORM Studio.

3

Challenge: Force a duplicate

Replay the same Stripe event id twice. Confirm the handler ran only once. Which column enforces that?

4

Challenge: Replay a stored event

Trigger a replay of a previously received event. Did your handler re-run against the stored payload? Why is replaying from storage safer than asking the provider to resend?