PingHook Documentation

Webhooks to Telegram, Slack, and Discord — no account, no dashboard, just POST.

Quick Start

1

Open Telegram and message @PingHookBot. Send /start. You receive a unique webhook URL — no email, no sign-up.

2

Send a test ping from your terminal:

curl -X POST https://pinghook.dev/send/YOUR_KEY/test \ -d "Hello from PingHook!"
3

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.

POST https://pinghook.dev/send/{api_key} POST https://pinghook.dev/send/{api_key}/{label}
FieldDescription
api_keyYour unique key — get it from /start in the bot
labelOptional. Any URL path after your key — slashes become part of the label: /send/KEY/github/ci/prod. Shows in every notification.
bodyAny 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:

https://pinghook.dev/send/YOUR_KEY/ci-failed

Hierarchical — slashes are part of the label:

https://pinghook.dev/send/YOUR_KEY/github/myapp/prod

No label:

https://pinghook.dev/send/YOUR_KEY
The label is the signal. When you own the code, fire the webhook only when the event is actually worth reading — and pick a label that says what happened. You usually don't need alerting rules at all.

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.

curl -X POST https://pinghook.dev/send/YOUR_KEY/ci-failed \ -H "Content-Type: application/json" \ -d '{"event":"test_failed","repo":"myapp","run":142}'

Plain Text & Markdown

Plain text is delivered as-is. Telegram renders a subset of Markdown natively.

SyntaxResult
**bold**Bold text
_italic_Italic text
~~strikethrough~~Strikethrough
`inline code`Monospace inline
curl -X POST https://pinghook.dev/send/YOUR_KEY/deploy \ -d "**Deploy succeeded** on main _By:_ asaf | _Duration:_ 42s | _Status:_ ~~failing~~ passing"
Slack and Discord channels receive their own formatted versions — JSON is wrapped in a code fence, plain text is delivered as-is with their respective Markdown syntax.

Responses & Errors

HTTPstatus fieldMeaning
200successDelivered to one or more channels
200suppressedFiltered by a rule or dedup — not an error
200failedAll channel deliveries failed (downstream issue)
401Unknown API key
413Payload exceeds 100 KB
429Rate limit exceeded — response includes resets_in (seconds)
Suppressed pings always return HTTP 200 with "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.

{ "event": "ci-run", "branch": "main", "errors": 14, "pinghook_rules": { "conditions": [ { "field": "errors", "operator": "gt", "value": 10 }, { "field": "branch", "operator": "eq", "value": "main" } ], "logic": "AND", "dedup": 30, "channel": "slack" } }

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

OperatorDescriptionType
eq / neqEquals / not equalsany
gt / ltGreater than / less thannumeric
gte / lteGreater or equal / less or equalnumeric
containsString contains (case-insensitive)string
existsField is present (any value)any

Shorthand keys inside pinghook_rules

KeyValueDescription
logicAND | ORCondition logic (default: AND)
dedupinteger (minutes)Suppress same label within N minutes
channeltelegram | slack | discordRoute 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.

ParamValueDescription
?channel=telegram | slack | discordRoute to one platform, ignore others
?dedup=N (integer, minutes)Suppress same label within N minutes
?silent=1 / true / yesLog but don't deliver — dry-run mode
?if=field:operator:valueJSON field condition. Repeatable → AND. Fails closed on non-JSON body.
?textif=wordBody 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:

curl "pinghook.dev/send/KEY/deploy?channel=slack" -d "done"

Suppress same-label repeats for 30 minutes:

curl "pinghook.dev/send/KEY/alert?dedup=30" -d "server high cpu"

Log but don't deliver — dry-run:

curl "pinghook.dev/send/KEY/test?silent=1" -d "test payload"

JSON field condition — only deliver when status equals firing:

curl "pinghook.dev/send/KEY/grafana?if=status:eq:firing"

Multiple ?if= params → AND logic:

curl "pinghook.dev/send/KEY/grafana?if=status:eq:firing&if=labels.env:eq:prod"

Text body contains (case-insensitive, repeatable → AND):

curl "pinghook.dev/send/KEY/logs?textif=error&textif=critical" -d "critical error in db"

Combine params — condition + platform routing + dedup:

curl "pinghook.dev/send/KEY/grafana?if=status:eq:firing&channel=telegram&dedup=5"
?if= fails closed: if the body isn't valid JSON, the ping is suppressed. For plain text or mixed sources, use ?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.

Global rules apply to every ping on your key. If you have multiple sources with different filtering needs, use query params on each source URL instead. Multi-key support is planned.

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.

CommandDescription
/startCreate account and get your unique webhook URL
/mykeyShow current webhook URL
/regen confirmRegenerate API key — old key is dead immediately
/channelsList 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)
/rulesList 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 confirmRemove all rules
/usagePing stats — today, this week, all time, last ping
/historyLast 10 successfully delivered pings with payload preview
/replay <n>Re-dispatch ping #n to all channels
/helpList 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

1

In Grafana: Alerting → Contact points → New contact point

2

Type: Webhook

3

URL: https://pinghook.dev/send/YOUR_KEY/grafana?if=status:eq:firing

4

Save and send a test notification to confirm it works

Example Grafana payload

{ "status": "firing", "receiver": "pinghook", "commonLabels": { "alertname": "HighCPU", "env": "production" }, "commonAnnotations": { "summary": "CPU > 90% for 5 minutes" }, "alerts": [{ "status": "firing" }] }

Useful filter combinations

Only prod alerts, only when firing:

?if=status:eq:firing&if=commonLabels.env:eq:production

Suppress duplicate alerts for 10 minutes:

?if=status:eq:firing&dedup=10

Route critical alerts to Telegram only, everything else to Slack (set as two separate contact points in Grafana):

?if=status:eq:firing&if=commonLabels.severity:eq:critical&channel=telegram
?if=status:eq:firing&channel=slack

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.

# .github/workflows/ci.yml - name: Notify via PingHook if: failure() run: | curl -s -X POST \ https://pinghook.dev/send/${{ secrets.PINGHOOK_KEY }}/ci-failed \ -H "Content-Type: application/json" \ -d '{ "repo": "${{ github.repository }}", "branch": "${{ github.ref_name }}", "run": ${{ github.run_number }}, "actor": "${{ github.actor }}" }'

Store your key in GitHub: Settings → Secrets and variables → Actions → New repository secret → PINGHOOK_KEY.

Different labels per outcome

- name: PingHook on failure if: failure() run: curl -s -X POST https://pinghook.dev/send/${{ secrets.PINGHOOK_KEY }}/ci-failed -d "Run ${{ github.run_number }} failed" - name: PingHook on success if: success() run: curl -s -X POST https://pinghook.dev/send/${{ secrets.PINGHOOK_KEY }}/ci-passed -d "Run ${{ github.run_number }} passed"

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

1

In Uptime Kuma: Settings → Notifications → Add Notification

2

Type: Webhook

3

URL: https://pinghook.dev/send/YOUR_KEY/uptime-kuma?if=heartbeat.status:eq:0

Uptime Kuma payload (example)

{ "heartbeat": { "status": 0, "msg": "Connection refused" }, "monitor": { "name": "My Service", "url": "https://mysite.com" } }
heartbeat.statusMeaning
0Down
1Up / recovered
2Pending
3Maintenance

Integration — n8n

Use PingHook as a notification step in any n8n workflow via the HTTP Request node.

SettingValue
MethodPOST
URLhttps://pinghook.dev/send/YOUR_KEY/n8n-alert
Body Content TypeJSON
JSON BodyAny fields from your workflow data

You can use n8n expressions in the URL for dynamic labels: https://pinghook.dev/send/KEY/{{ $json.eventType }}.

Drop PingHook at the end of n8n error-handling branches for instant failure notifications without a separate monitoring service.

Integration — Python

# pip install requests import requests PINGHOOK_URL = "https://pinghook.dev/send/YOUR_KEY"

JSON payload:

requests.post( f"{PINGHOOK_URL}/payment-received", json={"amount": 49.00, "customer": "jane@acme.com"} )

Plain text:

requests.post(f"{PINGHOOK_URL}/cron-done", data="ETL pipeline finished in 3.2s")

With dedup — suppress the same label for 30 minutes:

requests.post( f"{PINGHOOK_URL}/high-cpu", data="CPU spike detected", params={"dedup": 30} )

Minimal wrapper function

def ping(label: str, payload=None, **params): url = f"https://pinghook.dev/send/YOUR_KEY/{label}" kw = {"params": params} if isinstance(payload, dict): requests.post(url, json=payload, **kw) else: requests.post(url, data=payload or "", **kw) # Usage ping("payment-received", {"amount": 99}) ping("job-done", "Backup complete", dedup=60)

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):

curl -s -X POST https://pinghook.dev/send/YOUR_KEY/backup-done \ -d "Backup finished at $(date) — $(du -sh /backup | cut -f1)"

Ping only on failure — dead-man's switch:

/opt/myjob.sh || curl -s -X POST https://pinghook.dev/send/YOUR_KEY/job-failed \ -d "myjob.sh failed at $(date)"

In crontab with dedup — suppress repeated hourly alerts so only the first failure comes through:

0 * * * * /opt/health_check.sh || \ curl -s -X POST "https://pinghook.dev/send/YOUR_KEY/cron-failed?dedup=55" \ -d "health_check.sh failed at $(date)"
Tip: ?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

LimitFree tier
Per hour100 requests
Per day1,000 requests
Max payload size100 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:

git clone https://github.com/PingHook/PingHook && cd PingHook pip install -r requirements.txt

Create .env:

TELEGRAM_BOT_TOKEN=your_bot_token SUPABASE_URL=https://xxxx.supabase.co SUPABASE_KEY=your_service_role_key BASE_URL=https://your-domain.com ADMIN_SECRET=optional_admin_token

Apply the schema — paste schema.sql into the Supabase SQL editor and run it.

Start the server:

uvicorn app.main:app --host 0.0.0.0 --port 8000

Register the Telegram webhook (run once after every deploy):

python webhook.py
Local development needs a public tunnel. Run ngrok http 8000, set BASE_URL to the ngrok HTTPS URL, then run python webhook.py to register it with Telegram.