PingHook Documentation
Webhooks to Telegram, Slack, and Discord — no account, no dashboard, just POST.
Quick Start
Open Telegram and message @PingHookBot. Send /start. You receive a unique webhook URL — no email, no sign-up.
Send a test ping from your terminal:
The message arrives in Telegram within a second. That's it — paste the URL into Grafana, GitHub Actions, n8n, a cron job, or any HTTP client.
API Endpoint
One endpoint. No authentication headers — the API key is part of the URL path.
| Field | Description |
|---|---|
| api_key | Your unique key — get it from /start in the bot |
| label | Optional. Any URL path after your key — slashes become part of the label: /send/KEY/github/ci/prod. Shows in every notification. |
| body | Any payload — JSON or plain text. Max 100 KB. Content-Type header is optional. |
Labels
The label is everything after your API key in the URL. It appears as the notification header and can target rules. Labels support slashes for hierarchy.
Simple label:
Hierarchical — slashes are part of the label:
No label:
Payload Formats
JSON
JSON is auto-detected and pretty-printed in a monospace code block. You don't need a Content-Type header — PingHook tries JSON parsing first on every request.
Plain Text & Markdown
Plain text is delivered as-is. Telegram renders a subset of Markdown natively.
| Syntax | Result |
|---|---|
| **bold** | Bold text |
| _italic_ | Italic text |
| ~~strikethrough~~ | Strikethrough |
| `inline code` | Monospace inline |
Responses & Errors
| HTTP | status field | Meaning |
|---|---|---|
| 200 | success | Delivered to one or more channels |
| 200 | suppressed | Filtered by a rule or dedup — not an error |
| 200 | failed | All channel deliveries failed (downstream issue) |
| 401 | — | Unknown API key |
| 413 | — | Payload exceeds 100 KB |
| 429 | — | Rate limit exceeded — response includes resets_in (seconds) |
"status":"suppressed". Check the body if your script needs to distinguish deliveries from filtered events.
Layer 1 — pinghook_rules Highest Priority
For sources you own. Add a pinghook_rules key to your JSON body — PingHook evaluates it, strips it, and delivers the rest. Recipients never see it.
This delivers only when errors > 10 AND branch == "main", suppresses repeats for 30 minutes, and routes to Slack only. The delivered payload is everything minus pinghook_rules.
Use dot notation for nested fields: "field": "labels.env" or "field": "alerts.0.status".
| Operator | Description | Type |
|---|---|---|
| eq / neq | Equals / not equals | any |
| gt / lt | Greater than / less than | numeric |
| gte / lte | Greater or equal / less or equal | numeric |
| contains | String contains (case-insensitive) | string |
| exists | Field is present (any value) | any |
Shorthand keys inside pinghook_rules
| Key | Value | Description |
|---|---|---|
| logic | AND | OR | Condition logic (default: AND) |
| dedup | integer (minutes) | Suppress same label within N minutes |
| channel | telegram | slack | discord | Route to one platform only |
Layer 2 — Query Params Per-Request
No body changes needed. Append params to the webhook URL. Works for any source where you can customise the URL — Grafana, Uptime Kuma, most SaaS tools.
| Param | Value | Description |
|---|---|---|
| ?channel= | telegram | slack | discord | Route to one platform, ignore others |
| ?dedup= | N (integer, minutes) | Suppress same label within N minutes |
| ?silent= | 1 / true / yes | Log but don't deliver — dry-run mode |
| ?if= | field:operator:value | JSON field condition. Repeatable → AND. Fails closed on non-JSON body. |
| ?textif= | word | Body contains word (case-insensitive). Repeatable → AND. Works on any body type. |
?if= format: field.path:operator:value — dots for nesting, colon as separator.
Route to a specific platform only:
Suppress same-label repeats for 30 minutes:
Log but don't deliver — dry-run:
JSON field condition — only deliver when status equals firing:
Multiple ?if= params → AND logic:
Text body contains (case-insensitive, repeatable → AND):
Combine params — condition + platform routing + dedup:
?textif= instead.
Layer 3 — Global Bot Rules Fallback
Configured once in the Telegram bot via /rules. Applies to every ping on your key — use when you can't modify the body or the URL at all.
/rules add keyword error
Only deliver if payload contains "error" (case-insensitive)
/rules add dedup 30
Suppress same label repeated within 30 minutes
/rules add labels ci deploy
Only deliver pings whose label is "ci" or "deploy"
/rules remove 2
Remove rule #2. /rules clear confirm removes all.
Rule evaluation order
Layer 1 (pinghook_rules in body) → Layer 2 (query params) → Layer 3 (bot rules). Higher layers take full priority. When Layer 1 is present, the ?if= and ?textif= conditions in Layer 2 are skipped — but ?channel= and ?dedup= still apply as defaults that Layer 1 can override.
Bot Commands
All commands are sent to @PingHookBot in Telegram.
| Command | Description |
|---|---|
| /start | Create account and get your unique webhook URL |
| /mykey | Show current webhook URL |
| /regen confirm | Regenerate API key — old key is dead immediately |
| /channels | List all active delivery channels |
| /connect slack <url> | Add a Slack incoming webhook — validated with a test ping |
| /connect discord <url> | Add a Discord webhook — validated with a test ping |
| /disconnect <n> | Remove channel #n (from /channels list) |
| /rules | List active global rules |
| /rules add keyword <word> | Only deliver if payload contains this word |
| /rules add dedup <minutes> | Suppress same-label repeats within N minutes |
| /rules add labels <l1> <l2> … | Only deliver these labels (whitelist) |
| /rules remove <n> | Remove rule #n |
| /rules clear confirm | Remove all rules |
| /usage | Ping stats — today, this week, all time, last ping |
| /history | Last 10 successfully delivered pings with payload preview |
| /replay <n> | Re-dispatch ping #n to all channels |
| /help | List all commands |
Integration — Grafana
Grafana sends a JSON payload when an alert fires or resolves. Add ?if=status:eq:firing to only receive notifications when an alert fires — not when it resolves.
Setup
In Grafana: Alerting → Contact points → New contact point
Type: Webhook
URL: https://pinghook.dev/send/YOUR_KEY/grafana?if=status:eq:firing
Save and send a test notification to confirm it works
Example Grafana payload
Useful filter combinations
Only prod alerts, only when firing:
Suppress duplicate alerts for 10 minutes:
Route critical alerts to Telegram only, everything else to Slack (set as two separate contact points in Grafana):
Integration — GitHub Actions
Add a step at the end of any workflow. Use if: failure() to notify only on failures, or different labels per outcome.
Store your key in GitHub: Settings → Secrets and variables → Actions → New repository secret → PINGHOOK_KEY.
Different labels per outcome
Integration — Uptime Kuma
Add PingHook as a webhook notification. Use ?if=heartbeat.status:eq:0 to notify only when a monitor goes down, not on recovery.
Setup
In Uptime Kuma: Settings → Notifications → Add Notification
Type: Webhook
URL: https://pinghook.dev/send/YOUR_KEY/uptime-kuma?if=heartbeat.status:eq:0
Uptime Kuma payload (example)
| heartbeat.status | Meaning |
|---|---|
| 0 | Down |
| 1 | Up / recovered |
| 2 | Pending |
| 3 | Maintenance |
Integration — n8n
Use PingHook as a notification step in any n8n workflow via the HTTP Request node.
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://pinghook.dev/send/YOUR_KEY/n8n-alert |
| Body Content Type | JSON |
| JSON Body | Any fields from your workflow data |
You can use n8n expressions in the URL for dynamic labels: https://pinghook.dev/send/KEY/{{ $json.eventType }}.
Integration — Python
JSON payload:
Plain text:
With dedup — suppress the same label for 30 minutes:
Minimal wrapper function
Integration — Cron Jobs
Add one line to any shell script or crontab to get notified on failure — or on completion.
Ping on completion (append to any script):
Ping only on failure — dead-man's switch:
In crontab with dedup — suppress repeated hourly alerts so only the first failure comes through:
?dedup=55 on an hourly cron means the first failure pings you, but subsequent failures are silent until the job recovers — no alert storm.
Rate Limits
| Limit | Free tier |
|---|---|
| Per hour | 100 requests |
| Per day | 1,000 requests |
| Max payload size | 100 KB per request |
When exceeded, you get HTTP 429 with resets_in in the response body (seconds until the window resets). Rate-limited requests are still logged — check /usage in the bot for your counts.
Suppressed pings (filtered by rules) do not count against the rate limit.
Self-Hosting
PingHook is fully open source. Run your own instance with your own bot and database.
Requirements
- Python 3.11+
- A Telegram bot token (from @BotFather)
- PostgreSQL database (Supabase free tier works)
- Public HTTPS URL (Render, Railway, Fly.io free tiers all work)
Setup
Clone and install:
Create .env:
Apply the schema — paste schema.sql into the Supabase SQL editor and run it.
Start the server:
Register the Telegram webhook (run once after every deploy):
ngrok http 8000, set BASE_URL to the ngrok HTTPS URL, then run python webhook.py to register it with Telegram.