Integrations
Tell something other than your phone. Every integration keeps a per-event delivery log, so when Slack goes quiet you can see whether it was you or them.
| Kind | Needs | Notes |
|---|---|---|
| webhook | url. Optional secret; one is minted and shown once if you leave it out. | Signed. Payload below. |
| slack | webhook_url from an incoming-webhook app. | |
| telegram | token for a bot and a chat_id. | |
| apns | Nothing. Rejected if you send credentials. | Push to the iOS app. Recipients are the tenant’s members; the row exists so you can filter its events and read its delivery log. |
Credentials are sealed at rest and never returned by a read. Set -secret-key on a self-hosted daemon before creating any.
Create one
POST /v1/projects/{id}/integrations
{
"name": "ops-slack",
"type": "slack",
"config": { "webhook_url": "https://hooks.slack.com/services/…" },
"events": ["alert.opened", "alert.escalated", "alert.resolved"]
}
events narrows which alert events it receives: alert.opened, alert.bumped, alert.escalated, alert.acked, alert.resolved. Leave it out to receive all five. In the app: Integrations → New.
POST /v1/projects/{id}/integrations/{iid}/test sends a synthetic alert.test event synchronously and returns what the far end said, so you can see a 403 from Slack before you save. A disabled integration answers 409.
Webhook payload
{
"event": "alert.opened",
"alert_id": "…",
"project": "checkout",
"rule": "gateway-timeouts",
"template_sha256": "…",
"state": "firing",
"level": 400,
"count": 7,
"escalation_level": 0,
"message": "payment gateway timeout after 30012ms for order o_48113",
"at": 1756696364,
"identity": "gateway-timeouts",
"identifiers": { "gateway": "stripe" },
"page_accounts": ["…"]
}
identifiers and page_accounts are present only when the rule set them.
Verifying the signature
Two headers are sent: X-Pingtower-Timestamp, a unix timestamp, and X-Pingtower-Signature, the hex HMAC-SHA256 of "<timestamp>.<raw body>" under the endpoint’s secret. Recompute it over the exact bytes you received and compare in constant time. Reject timestamps older than a few minutes to close replays.
import hmac, hashlib
def verify(secret: bytes, timestamp: str, body: bytes, signature: str) -> bool:
mac = hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, signature)
Outbound URLs are checked against private and loopback ranges before delivery, so a webhook cannot be pointed at something inside the daemon’s own network.
Delivery log
GET /v1/projects/{id}/integrations/{iid}/deliveries?limit= lists attempts, newest first: event, alert_id, status, attempts, last_error, next_attempt_at. The app shows the same list under the integration.
Delivery is at-least-once through a durable outbox. A failed attempt retries with backoff starting at 30 seconds and doubling up to an hour. After ten consecutive failures the integration is disabled and stops trying; re-enable it with POST /v1/projects/{id}/integrations/{iid}/enable once the far end is fixed, which also clears the failure streak.