# Integrations

Source: https://www.pingtower.com/docs/service/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

<span class="pill post">POST</span> `/v1/projects/{id}/integrations`

```json
{
  "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](/docs/service/alerts/#who-gets-paged) it receives: `alert.opened`, `alert.bumped`, `alert.escalated`, `alert.acked`, `alert.resolved`. Leave it out to receive all five. In the app: Integrations → New.

<span class="pill post">POST</span> `/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

```json
{
  "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.

```python
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

<span class="pill get">GET</span> `/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 <span class="pill post">POST</span> `/v1/projects/{id}/integrations/{iid}/enable` once the far end is fixed, which also clears the failure streak.
