Skip to content

Commit 4e7d5bc

Browse files
committed
webhooks types and verification
1 parent 80987c7 commit 4e7d5bc

3 files changed

Lines changed: 771 additions & 0 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,46 @@ if err != nil {
154154
defer resp.Body.Close()
155155
```
156156

157+
## Webhooks
158+
159+
Loops delivers signed events to your configured endpoint. `VerifyWebhook` checks the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers against your dashboard signing secret (the `whsec_…` value), and `ParseWebhook` decodes the raw body into a typed event.
160+
161+
Read the raw request body **before** any JSON decoding — the signature is computed over the exact bytes.
162+
163+
```go
164+
func handler(w http.ResponseWriter, r *http.Request) {
165+
body, err := io.ReadAll(r.Body)
166+
if err != nil {
167+
http.Error(w, "bad request", http.StatusBadRequest)
168+
return
169+
}
170+
171+
if err := loops.VerifyWebhook(os.Getenv("LOOPS_SIGNING_SECRET"), r.Header, body); err != nil {
172+
http.Error(w, "invalid signature", http.StatusUnauthorized)
173+
return
174+
}
175+
176+
event, err := loops.ParseWebhook(body)
177+
if err != nil {
178+
http.Error(w, "bad payload", http.StatusBadRequest)
179+
return
180+
}
181+
182+
switch e := event.(type) {
183+
case *loops.WebhookContactCreatedPayload:
184+
log.Printf("contact created: %s", e.Contact.Email)
185+
case *loops.WebhookEmailDeliveredPayload:
186+
log.Printf("delivered to %s (%s)", e.ContactIdentity.Email, e.SourceType)
187+
default:
188+
log.Printf("unhandled event: %s", event.Type())
189+
}
190+
191+
w.WriteHeader(http.StatusOK)
192+
}
193+
```
194+
195+
Pass `loops.WithWebhookTimestampTolerance(5*time.Minute)` to `VerifyWebhook` to also reject stale deliveries (replay protection); it is off by default.
196+
157197
## License
158198

159199
MIT

0 commit comments

Comments
 (0)