FOR DEVELOPERS
Your website changes state.
We give your tools a hoot.
Receive a small JSON POST when a monitor confirms an outage, and optionally when it recovers. Available on Essential, Pro and Agency. View plans
Set up an endpoint
- Create a public endpoint that accepts
POSTwithContent-Type: application/json. - Open a monitor's edit form, expand Advanced Settings, and enter its Webhook URL under Custom Webhook. Use HTTP or HTTPS on port 80 or 443. HTTPS is recommended. Private networks, localhost, URL credentials and fragments are rejected.
- Enable Also send a webhook when the site recovers if you want recovery events. It is off by default and independent of email and SMS recovery settings.
- Save the monitor. Confirmed outages will now queue webhooks. Leave the URL blank to disable them.
Return a successful 2xx response directly. Redirects are not followed. The endpoint's DNS addresses are checked for public accessibility on every delivery.
Events & payload
monitor.down is sent once when the monitor reaches its configured consecutive-failure threshold. monitor.up is sent when that incident closes, if webhook recovery alerts are enabled. Healthy checks outside an incident, failed checks below the confirmation threshold, and SSL reminders do not send webhooks. Further failed checks during an open incident do not send another down event.
A monitor can be down even when http_status is 200. Use trigger to distinguish a website outage from a response-time or keyword failure, and reason for the diagnostic message. On recovery, these fields describe the original issue that has now resolved; http_status and response_time describe the recovery check. All configured checks must pass before a recovery event is sent.
| Trigger | Meaning |
|---|---|
| website_down | The website could not be reached, timed out, failed TLS validation, or returned an unsuccessful HTTP status. |
| response_time | The website responded successfully, but exceeded its configured response-time limit. |
| keyword_missing | The required text was missing from the response. |
| keyword_found | The forbidden text was present in the response. |
| check_failed | Another check failure, such as too many redirects or a response exceeding the monitoring size limit. |
{
"id": "a7dc5203-1b80-48fa-9772-834b1494dd52",
"event": "monitor.down",
"monitor": {
"id": 42,
"name": "My shop",
"url": "https://shop.example.com"
},
"status": "down",
"trigger": "website_down",
"reason": "Unexpected HTTP status: 503.",
"checked_at": "2026-09-14T12:00:00+00:00",
"http_status": 503,
"response_time": 350,
"downtime_seconds": null
}
| Field | Meaning |
|---|---|
| id | Unique event UUID. Preserved when you replay the event. |
| event | monitor.down or monitor.up. Use trigger to identify the type of issue. |
| monitor | The monitor ID, name and website URL at the time of the event. |
| status | down or up. This is the original event state, even on a later replay. |
| trigger | The issue category listed above. Recovery events retain the trigger of the incident that opened. |
| reason | Diagnostic message from the check that confirmed the incident; null if no diagnostic is available. Recovery events retain the original incident reason, even if later failed checks had a different reason. |
| checked_at | ISO 8601 timestamp of the check that confirmed the issue or recovery (UTC). |
| http_status | Website response code; null when the check received no HTTP response. This is separate from the webhook endpoint's response code. |
| response_time | Measured website response time in whole milliseconds for this check; null when unavailable, such as a connection failure. Included for both failure and recovery events. |
| downtime_seconds | null for a down event. For an up event, elapsed whole seconds from the first failed check to recovery, including confirmation time. This measures the issue duration, including response-time and keyword failures. |
A recovery is a new event with a new UUID. For example, if the first failed check was at 11:59:13, the outage was confirmed at 12:00:00, and recovery was checked at 12:00:25, the downtime is 72 seconds, even though the two event timestamps are only 25 seconds apart.
{
"id": "c7c54d1e-b9cf-4d6f-b376-8d27f0152c01",
"event": "monitor.up",
"monitor": {
"id": 42,
"name": "My shop",
"url": "https://shop.example.com"
},
"status": "up",
"trigger": "website_down",
"reason": "Unexpected HTTP status: 503.",
"checked_at": "2026-09-14T12:00:25+00:00",
"http_status": 200,
"response_time": 180,
"downtime_seconds": 72
}
Response-time failure
For example, a successful HTTP response taking 4,000 ms with a 2,000 ms limit produces the following event. Its recovery uses event: monitor.up and status: up, retains trigger: response_time and the original reason, and includes the newly measured response time.
{
"id": "81f5cdda-42c7-4e3d-87c0-c065f0375d72",
"event": "monitor.down",
"monitor": {
"id": 42,
"name": "My shop",
"url": "https://shop.example.com"
},
"status": "down",
"trigger": "response_time",
"reason": "Response time of 4000 ms exceeded the 2000 ms limit.",
"checked_at": "2026-09-14T13:00:00+00:00",
"http_status": 200,
"response_time": 4000,
"downtime_seconds": null
}
Request headers
Content-Type: application/json
Accept: application/json
User-Agent: PingHoot/1.0 (monitor webhook)
X-Site-Uptime-Event-Id: a7dc5203-1b80-48fa-9772-834b1494dd52
X-Site-Uptime-Delivery-Id: 123
X-Site-Uptime-Replay: false
Every replay has a new delivery ID and X-Site-Uptime-Replay: true. The event ID and original body remain unchanged. Standard HTTP transport headers are also recorded in the delivery log.
Receive a webhook
Save this example as webhook.php on your server, then enter its full URL in your monitor's Webhook URL field. PingHoot sends the event as JSON. No PingHoot token or API key is required.
This example reads the event and returns a successful response. Replace the comment with your own code to save or process it.
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$event = json_decode(file_get_contents('php://input'), true);
if (!is_array($event) || !is_string($event['id'] ?? null)
|| !in_array($event['event'] ?? null, ['monitor.down', 'monitor.up'], true)) {
http_response_code(400);
exit;
}
// Your code here: save the event or send it to your processing queue.
// $event['event'] is 'monitor.down' or 'monitor.up'.
// $event['trigger'] tells you what caused the alert.
http_response_code(204);
Return 204 after saving or accepting the event. If saving fails, return a 5xx response instead. Use $event['id'] to avoid processing the same event twice when it is replayed.
For a live integration: protect your receiving endpoint so it cannot accept alerts from just anyone. PingHoot does not sign webhook requests. One option is to add your own secret to the webhook URL and check it in your receiver; that secret is chosen by you, not issued by PingHoot.
If your framework uses CSRF protection, configure a dedicated webhook endpoint. Keep your normal website forms protected.
Delivery behaviour
- Requests run through the queue after the incident transaction commits. Delivery timing depends on worker availability.
- The connection timeout is 5 seconds and the HTTP request timeout is 10 seconds. A complete 2xx response marks the attempt successful; other status codes mark it failed.
- There are no automatic delivery retries. Review failures and replay them from the history screen.
- Requests are skipped if the account no longer has a paid plan or verified email, the monitor is deleted, or its webhook URL has changed. Automatic recovery deliveries also check that recovery alerts are still enabled.
- Delivery order is not guaranteed. Use the event timestamp when reconciling state. Replays can arrive long after an event occurred.
- A timeout or interrupted worker does not prove that the receiver did nothing. Store the event ID with a unique constraint and make processing idempotent so a replay cannot accidentally repeat a completed action.
Inspect history & replay
Open Webhooks in your dashboard to filter deliveries by monitor or status. Choose Details to inspect the method, URL, request headers and exact JSON body, response status, headers and complete received body, timing and errors. Binary response bodies are displayed as base64.
Logs are retained without an automatic expiry, independently of monitor-check history. They remain available after a downgrade or monitor deletion, and are removed when the account is deleted. URL, header, body and error fields are encrypted at rest. Logging begins when this feature is installed; past responses cannot be recovered.
| Queued | Waiting for a worker. |
|---|---|
| Sending | A worker has started this attempt. |
| Succeeded | The endpoint returned a complete 2xx response. |
| Failed | The endpoint returned a non-2xx response, or a validation, DNS or connection error occurred. An interrupted response contains only the bytes received. |
| Skipped | Current account or monitor settings prevented the request from being sent. |
| Unknown | The worker stopped without a recorded outcome. Check your receiver before replaying. |
Choose Replay on a completed attempt and confirm the destination. This creates a new delivery linked to the original, preserving its exact JSON body and URL. The monitor must still exist and use that URL; changing it back is necessary before replaying an older destination. An explicit replay of a recovery event is allowed even when automatic recovery webhooks are now disabled.
Replay requires a paid plan. View plans . Pending attempts cannot be replayed, only one replay of the same attempt can be pending at a time, and each account can request up to 10 replays per minute. Event IDs remain stable, so a receiver that has already processed the event may correctly acknowledge a replay without repeating its work.