How We Added Generic Webhooks to SendRec
Source: Dev.to
Webhook Overview
SendRec already supports Slack notifications, but Slack is just one channel.
If you need to trigger an n8n workflow when someone views a video, POST to your CRM when a viewer clicks a call‑to‑action, or pipe every event into a custom dashboard, you need generic webhooks – a single URL that receives every event as a JSON POST.
User Configuration
- Every user can configure a webhook URL in Settings.
- When something happens to their videos, SendRec
POSTs a JSON payload to that URL.
{
"event": "video.viewed",
"timestamp": "2026-02-21T14:30:00Z",
"data": {
"videoId": "abc-123",
"title": "Product Demo",
"watchUrl": "https://app.sendrec.eu/watch/xyz",
"viewCount": 5,
"viewerHash": "sha256..."
}
}
Event Types
Seven event types cover the full video lifecycle:
| Event | Description |
|---|---|
video.created | A new video record is created |
video.ready | The video has finished processing and is ready to watch |
video.deleted | The video has been removed |
video.viewed | A viewer watched the video |
video.comment | A comment was added |
video.milestone | A view‑count milestone was reached |
video.cta_click | A call‑to‑action button was clicked |
Payload Signing
Each request includes an X-Webhook-Signature header so the receiver can verify that the payload hasn’t been tampered with.
We use HMAC‑SHA256 and follow the same convention as GitHub webhooks (sha256= prefix).
func SignPayload(secret string, payload []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
- The secret is auto‑generated the first time a webhook URL is saved – 32 random bytes, hex‑encoded.
- The receiver recomputes the HMAC over the request body and compares it to the header value.
- The
sha256=prefix makes the algorithm explicit and leaves room for future algorithms without breaking existing integrations.
Retry & Delivery Logic
Webhook endpoints can go down, deployments restart, or load balancers hiccup. A single failed delivery must not mean a lost event.
Retry Policy
- Up to 3 attempts with exponential back‑off (
1s, then4s). - Configurable delays (
c.retryDelays). - Context cancellation aborts the wait, so shutdowns don’t block.
func (c *Client) Dispatch(ctx context.Context, userID, webhookURL, secret string, event Event) error {
body, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshal webhook payload: %w", err)
}
signature := SignPayload(secret, body)
maxAttempts := 1 + len(c.retryDelays)
var lastErr error
for attempt := 1; attempt = 200 && *statusCode maxResponseBodyBytes {
respBody = respBody[:maxResponseBodyBytes]
}
}
We read one byte more than the limit to detect truncation, then trim to exactly 1024 bytes.
Dispatch Helper (Fire‑and‑Forget)
The helper fires a webhook in a background goroutine, mirroring the existing Slack‑notification pattern.
func (h *Handler) dispatchWebhook(userID string, event webhook.Event) {
if h.webhookClient == nil {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
webhookURL, secret, err := h.webhookClient.LookupConfigByUserID(ctx, userID)
if err != nil {
return
}
_ = h.webhookClient.Dispatch(ctx, userID, webhookURL, secret, event)
}()
}
- For handlers that already run inside a goroutine (e.g., comment notifications, milestone recording, CTA click tracking), we call the webhook client directly—no extra goroutine is needed.
URL Validation & Secret Generation
- HTTPS only, with one exception:
http://localhostandhttp://127.0.0.1are allowed for local development. - URL length is capped at 500 characters.
- An empty URL clears the webhook (stored as
NULL) – no separate toggle required, same pattern as Slack.
When a webhook URL is saved for the first time and no secret exists, we generate one automatically:
func generateWebhookSecret() (string, error) {
b := make([]byte, 32) // 32 random bytes
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate secret: %w", err)
}
return hex.EncodeToString(b), nil
}
The generated secret is then used for the X-Webhook-Signature header on every dispatch.
TL;DR
- User‑configurable webhook URL → JSON payload per event.
- HMAC‑SHA256 signing with
X-Webhook-Signature. - Retry up to 3 times with exponential back‑off, full logging.
- Delivery rows stored in
webhook_deliveries(truncated response bodies). - Fire‑and‑forget dispatch that never blocks the user‑facing HTTP response.
- HTTPS enforced (localhost exception) and auto‑generated secret for each user.
All of this gives SendRec a robust, generic webhook system that can integrate with any downstream service.
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
Updating the Webhook URL
When you update the webhook URL, the existing secret is preserved using COALESCE:
INSERT INTO notification_preferences (user_id, webhook_url, webhook_secret)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
webhook_url = $2,
webhook_secret = COALESCE(notification_preferences.webhook_secret, $3);
If you need a new secret, use the dedicated “Regenerate” button. This creates a fresh secret via a separate endpoint from the one that saves the URL, so you don’t have to copy a new secret each time you change the endpoint.
Settings → Webhook Section
- URL input – field with a Save button.
- Signing secret – masked by default, with Copy and Regenerate buttons.
- Send test event – dispatches a
webhook.testevent so you can verify delivery without waiting for a real view. - Recent deliveries – shows the last 50 delivery attempts, each with:
- Event type
- Status badge
- Timestamp
- Expandable rows displaying the full payload and response body
- Events reference – a collapsible table that lists all event types and their payload fields.
The delivery log is the most useful part: when your webhook endpoint returns errors, you can see exactly what was sent and what came back, without digging through your server logs.
Try It Out
SendRec is open‑source (AGPL‑3.0) and self‑hostable. Generic webhooks are live at app.sendrec.eu:
- Go to Settings → Webhooks.
- Add a webhook URL.
- Click Send test event.
The webhook client implementation can be found in webhook.go.