PingHook Documentation

Webhooks to Telegram, Slack, and Discord โ€” no account, no dashboard, just POST.

Quick Start

1

Get your webhook URL from the bot โ€” Slack: type /pinghook in any channel  ยท  Telegram: message @PingHookBot and send /pinghook. 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 by sending /pinghook to 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 receives its own formatted version โ€” JSON is wrapped in a code fence, plain text is delivered as-is with Slack mrkdwn 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)
401โ€”Unknown API key
413โ€”Payload exceeds 100 KB
429โ€”Rate 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
channelslack | telegramRoute 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=slack | telegramRoute 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.
?ai=1 / claude / deepseekAppend an AI-generated triage card below the notification. 1 and claude use Claude Haiku; deepseek uses DeepSeek-V3.

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

AI triage โ€” analyse the payload and append an interpretation card (Claude Haiku):

curl "pinghook.dev/send/KEY/alert?ai=1" \ -H "Content-Type: application/json" \ -d '{"status":503,"service":"payments","error":"connection timeout"}'

Use DeepSeek-V3 instead:

curl "pinghook.dev/send/KEY/alert?ai=deepseek" \ -H "Content-Type: application/json" \ -d '{"status":503,"service":"payments","error":"connection timeout"}'

Layer 3 โ€” Global Bot Rules Fallback

Configured once via /pinghook rules in Slack or Telegram. Applies to every ping on your key โ€” use when you can't modify the body or the URL at all.

/pinghook rules add keyword error

Only deliver if payload contains "error" (case-insensitive)

/pinghook rules add dedup 30

Suppress same label repeated within 30 minutes

/pinghook rules add labels ci deploy

Only deliver pings whose label is "ci" or "deploy"

/pinghook rules remove 2

Remove rule #2. /pinghook 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.

AI Analysis

Append ?ai=1 to any webhook URL and PingHook will analyse the payload with an AI model and attach a triage card below your notification. No prompt engineering needed โ€” just add the param.

Providers

ParamModel
?ai=1Claude Haiku (default)
?ai=claudeClaude Haiku
?ai=deepseekDeepSeek-V3

Output format

The AI card is appended below the original payload, separated by a divider. It always follows this structure:

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿค– AI Analysis Summary: ๐Ÿ”ด Payment service timed out on the /charge endpoint โ€” 3 retries exhausted Severity: Critical HTTP 503: Server-side error โ€” the upstream service is unavailable or overloaded Suggested Next Step: Check payments pod health with kubectl get pods -n payments and tail logs on the failing replica

Field reference

FieldDescription
SummaryEmoji + plain-English interpretation of what happened and its impact. Emoji signals severity at a glance: ๐Ÿ”ด critical, ๐ŸŸก warning, ๐ŸŸข resolved, ๐ŸŽ‰ success, ๐Ÿ”” alert, ๐Ÿ’ณ payment, ๐Ÿš€ deploy, ๐Ÿ›‘ stopped.
SeverityOne of: Critical / Warning / Info / Success
HTTP [code]Only present when a 4xx or 5xx status code appears in the payload. Classifies as client-side or server-side and explains the specific code. Omitted for 2xx and when no HTTP status is found.
Suggested Next StepSpecific, actionable steps referencing real values from the payload โ€” file names, service names, error types. Never generic advice.

Behaviour notes

  • The label (URL segment after your key) is used as additional context when the payload alone is insufficient.
  • Timestamps in the payload (ISO 8601 or Unix epoch) are converted to relative human-readable form, e.g. "2 minutes ago".
  • Payloads are truncated to 2 000 characters before being sent to the AI.
  • If the AI call fails or times out, the original notification is delivered unchanged โ€” no ping is lost.
Explicit opt-in only. AI analysis is never applied globally โ€” you must add ?ai= to each source URL where you want it. This keeps analysis intentional and lets you control costs per source.

Bot Commands

All commands use the /pinghook prefix โ€” identical on both Slack and Telegram. In Slack, type them in any channel or DM with the bot. In Telegram, send them to @PingHookBot.

CommandDescription
/pinghookCreate account (if new) and get your unique webhook URL
/pinghook startSame as above โ€” explicit form
/pinghook mykeyShow current webhook URL
/pinghook regen confirmRegenerate API key โ€” old key is dead immediately
/pinghook channelsList all active delivery channels
/pinghook connect slack <url>Add a Slack incoming webhook โ€” validated with a test ping
/pinghook disconnect <n>Remove channel #n (from /pinghook channels list)
/pinghook rulesList active global rules
/pinghook rules add keyword <word>Only deliver if payload contains this word
/pinghook rules add dedup <minutes>Suppress same-label repeats within N minutes
/pinghook rules add labels <l1> <l2> โ€ฆOnly deliver these labels (whitelist)
/pinghook rules remove <n>Remove rule #n
/pinghook rules clear confirmRemove all rules
/pinghook usagePing stats โ€” today, this week, all time, last ping
/pinghook historyLast 10 successfully delivered pings with payload preview
/pinghook replay <n>Re-dispatch ping #n to all channels
/pinghook helpList all commands

Slack Bot Setup

Install the PingHook bot into your Slack workspace to use /pinghook slash commands and receive webhook notifications directly in Slack channels.

Step 1 โ€” Create a Slack App

1

Go to api.slack.com/apps โ†’ Create New App โ†’ From scratch. Name it PingHook and select your workspace.

Step 2 โ€” Add bot permissions

1

In the sidebar go to OAuth & Permissions โ†’ Bot Token Scopes.

2

Add these two scopes: commands and chat:write. Nothing else needed.

Step 3 โ€” Register the slash command

1

In the sidebar go to Slash Commands โ†’ Create New Command.

2

Fill in the fields:

FieldValue
Command/pinghook
Request URLhttps://pinghook.dev/slack/events
Short descriptionWebhook notifications via PingHook
Usage hintstart | mykey | rules | history | help

Step 4 โ€” Install and get your tokens

1

Go to Install App โ†’ Install to Workspace. Authorize it.

2

Copy the Bot User OAuth Token (starts with xoxb-) โ†’ this is your SLACK_BOT_TOKEN.

3

Go to Basic Information โ†’ Signing Secret โ†’ this is your SLACK_SIGNING_SECRET.

4

Add both to your .env and redeploy.

Step 5 โ€” Test it

In any Slack channel type /pinghook. You should receive a DM from the bot with your webhook URL.

Self-hosting note: update the Request URL in Step 3 to your own domain (e.g. https://yourdomain.com/slack/events).

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 /pinghook 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 SLACK_BOT_TOKEN=xoxb-your-slack-bot-token SLACK_SIGNING_SECRET=your_slack_signing_secret

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.