Skip to content

Generic webhook

The generic webhook kind is the lowest-level destination ErrLens offers: an HTTPS POST with a JSON body and an HMAC-SHA256 signature header. Anything that speaks HTTP can be a receiver — your own service, a serverless function, an existing internal alerting bus.

This is the original ErrLens alert path. The named kinds (Slack / Discord / Teams / n8n / email) are just opinionated formatters on top of this same machinery.

Payload shape

json
{
  "event": "issue.first_seen",
  "delivery_id": "fb1f...-uuid",
  "occurred_at": "2026-05-13T09:00:00Z",
  "project": {
    "id": "uuid",
    "slug": "checkout-api",
    "name": "checkout-api",
    "platform": "go"
  },
  "issue": {
    "id": "uuid",
    "title": "panic: runtime error: invalid memory address",
    "culprit": "handlers/charge.go:142",
    "level": "fatal",
    "environment": "prod",
    "release": "checkout-api@4.2.1",
    "times_seen": 1,
    "first_seen": "2026-05-13T09:00:00Z",
    "url": "https://acme.errlens.dev/projects/checkout-api/issues/uuid"
  }
}

eventissue.first_seen | project.frequency. Schema is identical across triggers.

Headers

HeaderValue
Content-Typeapplication/json
User-Agenterrlens-webhook/<version>
X-ErrLens-Eventsame as the event field
X-ErrLens-Deliverythe delivery's UUID — use for idempotency
X-ErrLens-Kindgeneric_webhook
X-ErrLens-Signaturesha256=<hex> — HMAC-SHA256 of the raw body

Verifying the signature

The signature is HMAC-SHA256 over the raw request body bytes using the destination's signing secret. Constant-time-compare on the receiver side. Examples:

Node

js
import crypto from "node:crypto";

function verify(secret, rawBody, sigHeader) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return expected.length === sigHeader.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader));
}

Go

go
func verify(secret string, body []byte, sig string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(sig))
}

Python

python
import hmac, hashlib

def verify(secret: str, body: bytes, sig: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

Add it to ErrLens

  1. Open Org alerts (or a project's Alerts tab).
  2. Add destination → pick Generic webhook.
  3. Paste the receiver URL → set a destination name → save.
  4. ErrLens mints a fresh HMAC key. Copy it once — it's shown in plaintext on the destination row but rotation = delete + re-add.
  5. Configure the receiver with the same key. Send test event to confirm end-to-end.

Retry policy

The dispatcher uses River as the queue, with exponential backoff:

StatusWhat happens
2xxSuccess. Counters reset.
4xx (not 408/429)Permanent. Job cancelled, no retry.
408, 429, 5xxTransient. Retried with backoff up to 6 attempts.
Network error / timeoutTransient. Retried with backoff.

Idempotency: every delivery carries a unique X-ErrLens-Delivery UUID. The same delivery may be retried multiple times after a transient failure — receivers that mutate state should dedupe on this header.

When to use this kind over the named ones

Pick generic when:

  • You're integrating with an internal service that already speaks HMAC-signed webhooks
  • You need a paper trail (signature → known sender) for compliance
  • You want raw JSON to fan to multiple downstream systems (use n8n for the visual-workflow version of this)

Pick a named kind (Slack / Discord / Teams / Email) when:

  • You want a pre-rendered, opinionated card / message
  • You don't want to write any receiver code