SitePulse can deliver issue lifecycle alerts to URLs you control. This guide covers webhook-style channels: the generic Webhook channel, Slack webhook, Microsoft Teams, connected Slack, and connected Discord.
Email and in-app notifications use separate delivery paths. See Notifications for the full channel overview.
Configuration fields
| Field | Applies to | Description |
|---|
| Channel | All | webhook (your own HTTP endpoint), slack (incoming webhook URL), slack_app (OAuth connection; SitePulse stores the webhook URL after you connect), discord (OAuth connection; SitePulse stores the webhook URL after you connect), or microsoft_teams (workflow incoming webhook URL) |
| Destination | URL channels | Public http:// or https:// webhook URL, max 2,048 characters |
| HTTP method | webhook only | post (default) or get. Slack, Slack app, and Teams always use POST. |
| Events | All | open (Issue detected — new issues and reopens, plus escalation follow-ups) and/or resolve (Issue resolved — automatic monitoring confirms recovery) |
| Tool scope | All | All tools, or a subset of enabled check tools |
| Enabled | All | Toggle delivery without deleting the configuration |
Configurations can be tested with a sample payload before and after saving (see Test payload).
A configuration delivers an alert only when all of the following are true:
- The configuration is enabled
- The issue transition matches a subscribed event (
open, resolve, or escalate via an Issue detected subscription)
- The issue’s check tool is in scope (all tools, or listed in the configuration)
- The team is not in quiet time
- The destination URL passes SitePulse’s outbound safety checks
Acknowledging or refreshing an issue does not send webhook alerts. Open (including reopen), resolve, and escalate transitions can trigger delivery when subscribed.
Delivery behavior
Webhook alerts are sent asynchronously after the issue transition commits to the database.
| Behavior | Detail |
|---|
| Timing | Queued job; not synchronous with the check that opened the issue |
| Quiet time | Suppresses all outbound issue alerts (webhooks, email, in-app) |
| Connect timeout | 5 seconds |
| Total timeout | 10 seconds |
| Retries | Up to 3 attempts at 200ms, 500ms, and 1000ms intervals |
| Redirects | Disabled |
| Response handling | Non-2xx responses are logged; monitoring and issue state are unaffected |
| Failures | Exceptions are logged and swallowed — they do not roll back the issue |
SitePulse does not retry failed webhooks beyond the short HTTP retry window. Build idempotent handlers and do not rely on webhooks as the only record of an incident.
POST (recommended)
SitePulse sends Content-Type: application/json with Accept: application/json.
Generic webhook — raw issue lifecycle JSON (see Production payload below).
Slack webhook / Slack app — Slack Block Kit message JSON with text and blocks fields.
Discord — Discord webhook JSON with content and embeds fields.
Microsoft Teams — Office 365 Connector MessageCard JSON.
GET (generic webhook only)
SitePulse sends the production payload as query string parameters on the configured URL. Nested objects are encoded using Laravel’s query serialization (e.g. issue[summary]=..., team[id]=...).
Use GET only when your receiver cannot accept POST bodies. POST is recommended for nested payloads.
All HTTP webhook deliveries include channel-specific headers where applicable:
| Header | Test delivery | Issue delivery | Value |
|---|
X-SitePulse-Event | notification.test | issue.opened, issue.recovered, or issue.escalated | Event name |
X-SitePulse-Delivery | test | issue-lifecycle | Delivery type |
The X-SitePulse-Event header mirrors the event field in the JSON body (for POST) or query payload (for GET).
Event mapping
| UI label | Internal transition | event value | When it fires |
|---|
| Issue detected | open | issue.opened | New issue opens |
| Issue detected | open (from reopen) | issue.opened | Previously resolved issue fails again |
| Issue resolved | resolve | issue.recovered | Automatic monitoring confirms recovery |
| Issue escalation | escalate | issue.escalated | Follow-up reminder while issue stays open or acknowledged |
The transition field in the payload reflects the internal transition (open, resolve, or escalate), not reopen.
Production payload
Generic Webhook channel deliveries use this JSON shape on POST:
{
"event": "issue.opened",
"transition": "open",
"occurred_at": "2026-05-25T12:00:00+00:00",
"team": {
"id": 1,
"name": "Example Team"
},
"issue": {
"id": 123,
"type": "status",
"status": "open",
"severity": "critical",
"fingerprint": "status:down",
"summary": "Example is down",
"link": "https://app.sitepulse.dev/dashboard/issues?status=active&site=..."
},
"site": {
"id": 10,
"uuid": "9d4e2c8f-1234-5678-9abc-def012345678",
"name": "Example",
"url": "https://example.com",
"domain": "example.com"
}
}
Field reference
| Field | Type | Description |
|---|
event | string | issue.opened, issue.recovered, or issue.escalated |
transition | string | open, resolve, or escalate |
occurred_at | string | ISO 8601 timestamp when the alert was generated |
team.id | integer | Team id |
team.name | string | Team display name |
issue.id | integer | Internal issue id |
issue.type | string | Check tool: status, ssl, dns, broken-links, performance |
issue.status | string | Issue status at time of delivery (open, acknowledged, or resolved) |
issue.severity | string | critical, warning, or info |
issue.fingerprint | string | Stable deduplication key (e.g. status:down, ssl:expiring) |
issue.summary | string | Human-readable issue title |
issue.link | string | URL to the filtered Issues page in SitePulse |
site.id | integer | Internal site id |
site.uuid | string | Site UUID (used in API paths) |
site.name | string | Site display name |
site.url | string | Monitored URL |
site.domain | string | Hostname derived from the URL |
Read issue history programmatically with the Issues API (issues:read scope). Webhook issue.id is the internal integer id, not the UUID exposed by the API.
Test payload
The Test button sends a different payload from production issue alerts. Use it to verify connectivity, not to validate your issue handler.
{
"event": "notification.test",
"sent_at": "2026-05-25T12:00:00+00:00",
"team": {
"id": 1,
"name": "Example Team"
},
"configuration": {
"id": 42,
"channel": "webhook",
"destination": "https://hooks.example.com/sitepulse"
},
"message": "This is a test webhook from SitePulse."
}
Test deliveries set X-SitePulse-Delivery: test and X-SitePulse-Event: notification.test.
Slack and Microsoft Teams test messages use channel-specific formatting (Slack blocks or Teams MessageCard) with the same test metadata.
You can test unsaved modal values from the configuration dialog before creating or updating a configuration.
Slack (app and webhook)
Production Slack messages are POSTed as JSON with:
text — short summary line
blocks — formatted sections for summary, site, tool, severity, status, and a View issue button linking to issue.link
Titles:
- Issue detected:
SitePulse detected an issue: {site name}
- Issue resolved:
SitePulse recovered: {site name}
Discord
Production Discord messages are POSTed as JSON with:
content — short summary line
embeds — formatted fields for site, tool, severity, status, and a link to the issue
Discord uses red embeds for detected issues and green embeds for recoveries.
Microsoft Teams
Production Teams messages are POSTed as MessageCard JSON with:
themeColor — red (DC2626) for detected issues, green (16A34A) for recoveries
title, text, and sections with site/tool/severity/status facts
potentialAction with a View issue link
Destination URL requirements
Webhook URLs must be:
- Valid
http:// or https:// URLs
- Publicly reachable from SitePulse workers
- Free of embedded credentials (
user:pass@host is rejected)
SitePulse blocks destinations that resolve to private, loopback, or link-local addresses. This includes hostnames like localhost, *.local, and hosts whose DNS resolves to non-public IPs.
Internal network URLs (private RFC 1918 ranges, metadata endpoints, etc.) are rejected by design.
Example receiver
Minimal POST handler (Node.js / Express):
app.post('/sitepulse/hooks', express.json(), (req, res) => {
const event = req.get('X-SitePulse-Event');
if (event === 'notification.test') {
console.log('Test webhook received');
return res.sendStatus(200);
}
if (event === 'issue.opened') {
console.log(`Issue opened: ${req.body.issue.summary}`);
// Page on-call, create ticket, etc.
}
if (event === 'issue.recovered') {
console.log(`Issue recovered: ${req.body.issue.summary}`);
// Close ticket, notify channel, etc.
}
res.sendStatus(200);
});
Return any 2xx status promptly. SitePulse treats other responses as delivery failures.
Integration recipes
Event type reference
Use this table when building idempotent handlers for the generic Webhook channel (POST JSON):
X-SitePulse-Event | event (body) | transition | Suggested action |
|---|
notification.test | notification.test | — | Verify connectivity only |
issue.opened | issue.opened | open | Page on-call, open ticket, post to chat |
issue.escalated | issue.escalated | escalate | Send a reminder for an active issue |
issue.recovered | issue.recovered | resolve | Resolve ticket, notify recovery |
Slack, Slack webhook, Discord, and Microsoft Teams channels receive formatted payloads, not the raw issue JSON above. Use the generic Webhook channel for custom integrations.
Route SitePulse alerts to PagerDuty using a generic webhook and a small translation layer.
- Create a Webhook configuration: method POST, events Issue detected and Issue resolved, destination set to your middleware URL.
- Map SitePulse events to PagerDuty
trigger and resolve actions using X-SitePulse-Event or event in the body.
- Use
issue.fingerprint combined with site.uuid as the PagerDuty dedup_key so reopen and recovery events update the same incident.
Example middleware (forwards to PagerDuty Events API v2):
app.post('/sitepulse/pagerduty', express.json(), async (req, res) => {
const event = req.get('X-SitePulse-Event');
const { issue, site } = req.body;
if (event === 'notification.test') {
return res.sendStatus(200);
}
const dedupKey = `${site.uuid}:${issue.fingerprint}`;
const summary = issue.summary || `SitePulse alert for ${site.name}`;
const payload = {
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: event === 'issue.recovered' ? 'resolve' : 'trigger',
dedup_key: dedupKey,
payload: {
summary,
severity: issue.severity === 'critical' ? 'critical' : 'warning',
source: site.domain,
custom_details: issue,
},
};
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
res.sendStatus(200);
});
Store PAGERDUTY_ROUTING_KEY in your middleware environment, not in SitePulse.