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.
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.