Webhook Templates
Shape incoming webhook payloads into structured Alphorn notifications with per-webhook template fields - no external proxies needed.
Many tools send webhooks in their own shape - GitHub, Sentry, Prometheus, and others each use different payload formats. Alphorn turns these into structured notifications without any external proxy or transform service.
You have three options, from most to least work on the sender side:
- Configure the sender natively - if the service supports custom webhook body templating (Grafana, Healthchecks.io, Gatus), make it send Alphorn's native format directly. See Integrations.
- Use Alphorn's per-webhook template fields - for services with fixed payload shapes (GitHub, Sentry, Prometheus Alertmanager), fill in the four template fields on the webhook (or start from a source preset) and Alphorn extracts fields from the body.
- Rely on the heuristic fallback - leave the templates empty and Alphorn picks reasonable defaults from common field names.
This page covers options 2 and 3.
Template fields
Each webhook has four optional template fields under Advanced: field templates, both when creating a webhook and in Webhooks → (your webhook) → Edit:
| Field | Produces | Notes |
|---|---|---|
| Title template | title (≤ 200 chars) | Short headline, truncated if longer |
| Message template | message (≤ 4000 chars) | Notification body, truncated if longer |
| Tags template | tags[] | Comma-separated; each part trimmed, empty parts dropped |
| Priority template | priority (1-5) | Must resolve to a number. Rounded and clamped into 1-5. A non-numeric value means no priority; an empty result falls back to the body |
Each template can be up to 500 characters long.
Syntax
Templates use {dotted.path} placeholders resolved against the JSON request body:
{title}→body.title{repository.full_name}→body.repository.full_name{alerts.0.labels.severity}→ the first entry of thealertsarray (array items are addressed by index)
Missing paths, objects, and arrays render as empty; strings, numbers, and booleans are inserted as text. There are no conditionals, loops, or helpers - just substitution. That's intentional: templates stay predictable and can't be used to smuggle logic or secrets.
Templates only apply to JSON bodies. Plain-text requests use the X-Title, X-Priority, and X-Tags headers instead (see Webhooks).
Request headers
Request headers are available under $headers, using lower-case header names - for example {$headers.host}. Placeholders only allow letters, digits, _, and ., so headers with a hyphen in their name (like X-GitHub-Event or User-Agent) can't be referenced.
Sensitive headers are removed before templates see them: Authorization, Cookie, Set-Cookie, Proxy-Authorization, and anything starting with X-Auth or X-Api.
Order of precedence
For title, message, and tags, Alphorn uses the first source that produces a value:
- The webhook's template for that field
- For Slack-compatible payloads: the Slack
text, blocks, or attachments (title and message only) - The heuristic fallback below
- For the message only: the whole JSON body, pretty-printed
Priority is slightly different: if the priority template resolves to an empty value (e.g. the field is missing), Alphorn falls back to the body's numeric priority field. If it resolves to something that isn't a number (e.g. critical), the notification has no priority - there is no fallback in that case.
Heuristic fallback
If a template is empty, Alphorn walks the body plus common wrapper keys (issue, pull_request, data, event, submission, object, payload, form_response) and picks the first string-valued field from:
- Title candidates:
title,subject,name,headline - Message candidates:
message,body,msg,text,content,description,summary
Top-level priority (number, clamped into 1-5) and tags (array or comma-separated string) are read directly. If present, a top-level action string is prefixed to the title (opened: Some issue title).
If nothing matches, the message renders as the full JSON body.
Source presets
When creating a webhook, the Source preset (optional) selector pre-fills the four template fields for common services. You can adjust the fields afterwards - the preset is only a starting point.
| Preset | Title | Message | Tags |
|---|---|---|---|
| GitHub | {action}: {repository.full_name} | {issue.title}{pull_request.title} | github,{repository.name} |
| Grafana | {title} | {message} | grafana,{state} |
| Prometheus Alertmanager | {commonAnnotations.summary} | {commonAnnotations.description} | alertmanager,{status} |
| Sentry | {data.issue.title} | {data.issue.culprit} | sentry,{data.issue.level} |
| Stripe | {type} | {data.object.description} | stripe |
The presets leave the priority template empty, because these services don't send a numeric priority. The examples further down this page are more detailed alternatives you can paste in instead.
GitHub
The event type is only carried in the X-GitHub-Event header, which templates can't reference (header names with a hyphen aren't allowed in placeholders) - so create one Alphorn webhook per GitHub event type and have GitHub deliver only that event to it. GitHub can't send an Authorization header, so leave Require API key off for these webhooks.
Pull requests
Title: PR {action}: {pull_request.title}
Message: {sender.login} {action} PR #{pull_request.number} in {repository.full_name}
Tags: github,pr,{action},{repository.name}
Priority: 3Issues
Title: Issue {action}: {issue.title}
Message: {sender.login} {action} issue #{issue.number} in {repository.full_name}
Tags: github,issue,{action},{repository.name}
Priority: 2Pushes
Title: Push to {repository.full_name}
Message: {pusher.name} pushed to {ref} - {head_commit.message}
Tags: github,push,{repository.name}
Priority: 2Sentry
Sentry's default webhook plugin sends events under data.issue:
Title: Sentry: {data.issue.title}
Message: Culprit: {data.issue.culprit} - Level: {data.issue.level} - Events: {data.issue.count}
Tags: sentry,{data.issue.level},{data.issue.project.slug}
Priority: 4PagerDuty (incoming)
Fan PagerDuty incidents out to other channels with a PagerDuty webhook subscription (v3) pointing at an Alphorn webhook:
Title: PagerDuty: {event.data.title}
Message: Service: {event.data.service.summary} - Urgency: {event.data.urgency} - Status: {event.data.status}
Tags: pagerduty,{event.data.urgency},{event.data.status}
Priority: 5Prometheus Alertmanager
Alertmanager sends arrays of alerts with commonLabels and commonAnnotations rollups. Use those to describe the whole group, or {alerts.0.…} for the first alert only:
Title: {commonLabels.alertname}
Message: {commonAnnotations.summary}
Tags: prometheus,{status},{commonLabels.severity}
Priority: 4Tips
- Priority can't branch on values. If you need different priorities per severity (critical = 5, warning = 3), either create separate webhooks - one per severity - or keep priority constant and route severities to different channels via filter rules.
- Narrow event delivery at the source. Most services let you pick which events each webhook URL receives. Combining that with dedicated Alphorn webhooks keeps templates simple.
- The full body is always stored as
payloadand available for filtering rules, so you don't have to cram everything into the templated message.
Using payload for routing
The full JSON body is stored on every message as payload, regardless of templating. Filter on it to route the same webhook to different channels. Paths are relative to the body, so for a GitHub push event:
| Channel | Filter | Purpose |
|---|---|---|
| PagerDuty | Payload ref equals refs/heads/main | Only alert on pushes to main |
| Slack (#frontend) | Payload repository.name equals frontend | Frontend repo activity |
| Slack (#backend) | Payload repository.name equals api | Backend repo activity |
See Filtering Rules → Payload paths for details.
Direct forwarding
For trivial cases, point any webhook at Alphorn without filling in any template. The raw body is accepted and the heuristic picks a reasonable title and message from common field names. Add templates later when you want more polished notifications.
Stream notifications (SSE)
GET /api/stream/{publicId}: receive Alphorn notifications in real time over Server-Sent Events. Event format, connection limits, and errors.
Integrations
Send alerts to Alphorn from Grafana, Prometheus Alertmanager, Uptime Kuma, GitHub Actions, Jenkins, Home Assistant, Sonarr, and other tools via webhooks.