Webhooks
A webhook is an inbound URL that turns arbitrary JSON into a published message on one topic. Use them to wire third-party services into your topics without writing a custom integration.
How it works
- You create a webhook attached to a topic, with a template that maps fields from incoming JSON into a message payload.
- pingd generates a one-time bearer-style token and returns the receive URL:
POST /hooks/:token. - The third-party service
POSTs JSON to that URL. - pingd renders the template against the JSON, publishes the message.
- Normal dispatch fanout happens (push, SSE, history).
Create a webhook
curl -s http://localhost:7685/topics/ci.github/webhooks \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"template": {
"title": "{{repository.full_name}} push",
"body": "{{head_commit.author.name}}: {{head_commit.message}}",
"tags": "github,{{repository.name}}",
"priority": 2
}
}'
pingd-cli webhooks create \
--topic ci.github \
--template template.json
import requests
requests.post(
"http://localhost:7685/topics/ci.github/webhooks",
headers={"Authorization": "Bearer TOKEN"},
json={
"template": {
"title": "{{repository.full_name}} push",
"body": "{{head_commit.author.name}}: {{head_commit.message}}",
"tags": "github,{{repository.name}}",
"priority": 2,
},
},
)
Response (token shown once):
{
"id": "uuid",
"topicID": "uuid",
"token": "whk_...",
"template": { "...": "..." },
"createdAt": "..."
}
The receive URL is:
POST /hooks/whk_...
Template fields
| Field | Type | Notes |
|---|---|---|
title | template string | Optional message title |
subtitle | template string | Optional subtitle |
body | template string | Falls back to the raw request body if empty |
tags | comma-separated template | Split on commas, trimmed |
priority | 1–3 | Optional, default 2 |
ttl | seconds | Optional, max 30 days |
Template syntax
Templates use {{path.to.field}} lookups against the incoming JSON. Examples:
{{alert.title}}
{{source}}
{{event.repository.full_name}}
Missing fields render as empty string. There are no conditionals or loops, by design.
Use a number in the path to access an array element by position (zero-indexed):
{{episodes.0.title}}
{{users.1.name}}
{{tags.0}}
Out-of-range indexes render as empty.
You can also render a whole array. {{tags}} joins its elements with , :
input: { "tags": ["release", "urgent"] }
output: "release, urgent"
Nested arrays, objects, and null inside the array are skipped.
Receive payload
Webhook receive does not use bearer auth. The token in the URL is the credential.
curl -s http://localhost:7685/hooks/whk_example \
-H 'Content-Type: application/json' \
-d '{
"alert": { "title": "Disk full" },
"source": "nas",
"message": "Volume /data is 95% full",
"service": "storage"
}'
Successful receives return 202 Accepted.
Endpoints
| Method | Path | Auth |
|---|---|---|
| GET | /topics/:name/webhooks | Owner / admin |
| POST | /topics/:name/webhooks | Owner / admin |
| GET | /webhooks/:id | Owner / admin |
| PATCH | /webhooks/:id | Owner / admin |
| DELETE | /webhooks/:id | Owner / admin |
| POST | /hooks/:token | Token in URL only |
rw
permission on a topic cannot create webhooks for it.
Worked example: GitHub push events
Point GitHub's webhook at https://your-pingd/hooks/whk_.... For a payload like:
{
"repository": {
"full_name": "acme/inventory",
"name": "inventory"
},
"head_commit": {
"author": { "name": "alice" },
"message": "fix tax rounding"
}
}
The template renders a message on ci.github:
title: acme/inventory push
body: alice: fix tax rounding
tags: github, inventory
Save your template to a file (template.json):
{
"title": "{{repository.full_name}} push",
"body": "{{head_commit.author.name}}: {{head_commit.message}}",
"tags": "github,{{repository.name}}",
"priority": 2
}
Rate limiting
Webhook receives are rate-limited per token and per IP, separately from the general API.
Tune them with
PINGD_WEBHOOK_RATE_LIMIT_PER_TOKEN and PINGD_WEBHOOK_RATE_LIMIT_PER_IP.
Security notes
- The webhook token is a bearer credential. Anyone with the URL can publish.
- Tokens are shown once on creation. Save them then.
- Deleting a webhook invalidates its URL immediately.
- Token rotation is on the roadmap; for now, rotate by deleting + recreating.
- Logging redaction for webhook tokens is on the roadmap; treat request logs as sensitive.