Webhooks let you connect Pingara with any system that can receive HTTP POST requests, such as custom dashboards, ticketing systems, ChatOps bots, automation platforms, or your own backend services.
How Webhooks Work
When an alert condition is met, Pingara sends an HTTP POST request to your configured endpoint with a JSON payload containing all incident details.
Pingara detects incident
↓
POST https://your-endpoint.com/pingara-webhook
Content-Type: application/json
↓
Your system receives and processes the event
Setting Up a Webhook
Step 1: Prepare Your Endpoint
Create an HTTP endpoint that:
- Accepts POST requests
- Parses JSON request bodies
- Returns a 2xx status code on success
- Is publicly accessible from the internet
Example (Node.js/Express):
app.post('/pingara-webhook', (req, res) => {
const event = req.body;
console.log('Pingara event:', event.event, event.monitor.name);
// Process the event...
res.status(200).json({ received: true });
});
Step 2: Add the Webhook in Pingara
- Go to Settings → Alert Policies → [Your Policy]
- Click Add Channel
- Select Webhook
- Enter your endpoint URL
- Click Save
Step 3: Test the Webhook
Click Send Test to send a sample event to your endpoint. Verify:
- Your endpoint receives the request
- The JSON payload is parsed correctly
- Your endpoint returns 200 OK
Payload Format
Standard Payload Structure
Every incident-related webhook event follows this structure. monitor carries
only id, name and url, with no status or region, so infer the
status from the event name. incident only carries the fields
below; there is no status, startedAt, resolvedAt, or performance
metrics block, and there is no top-level organization object:
{
"event": "incident.created",
"timestamp": "2024-01-15T14:32:05.000Z",
"monitor": {
"id": "m1aa2b3c4d5e6f",
"name": "API Production",
"url": "https://api.example.com/health"
},
"incident": {
"id": "i9zz8y7x6w5v4u",
"errorType": "timeout",
"errorMessage": "Connection timeout after 30000ms",
"affectedRegions": ["us-east-1", "eu-west-1"]
}
}
incident.duration (a formatted string like "12 mins, 4 secs", not a
number of seconds) is only present on incident.resolved and
incident.escalated events. incident.escalationCount is only present on
incident.escalated. Both are omitted entirely, not sent as null,
when they don't apply.
Event Types
| Event | Trigger | Incident field present |
|---|---|---|
incident.created | An incident enters the down state, either opened fresh (2 consecutive failures, quorum-confirmed) or as an already-open degraded incident promoted to down. Same event either way; there is no separate "promoted" event | Yes |
incident.degraded | New incident opened for a degraded transition, from sustained slow responses or a keyword mismatch on a monitor whose keyword-failure severity is Degraded | Yes |
incident.resolved | Incident resolved automatically (2 consecutive successes, quorum-confirmed) | Yes, includes duration |
incident.escalated | An unacknowledged incident reaches its next escalation step. Escalation is keyed on the incident still being open and unacknowledged, not on its severity, so a degraded incident escalates on the same schedule as a down one | Yes, includes duration and escalationCount |
ssl.expiring | A certificate crosses a configured expiry-warning threshold | No, an ssl block instead |
domain.expiring | A monitored domain's registration crosses a configured expiry-warning threshold | No, a domain block instead |
monitor.paused | Monitor was paused | No |
monitor.resumed | Monitor was resumed | No |
monitor.monitoring_interrupted | Every probe region configured for this monitor is in a confirmed blackout, so Pingara cannot check it. This says nothing about your endpoint | Yes, an incident-shaped block with errorType: "monitoring_interrupted", but there is no incident record behind it |
monitor.monitoring_resumed | The probe regions came back and checks are running again | Yes, same shape, errorType: "monitoring_interrupted" |
There is no incident.updated event. A manual status change (Investigating
→ Identified → Monitoring) doesn't dispatch a webhook. See
Understanding Incidents for what
manual status changes do and don't notify.
Event-Specific Payloads
incident.resolved:
{
"event": "incident.resolved",
"timestamp": "2024-01-15T14:44:00.000Z",
"monitor": {
"id": "m1aa2b3c4d5e6f",
"name": "API Production",
"url": "https://api.example.com/health"
},
"incident": {
"id": "i9zz8y7x6w5v4u",
"errorType": "timeout",
"errorMessage": "Connection timeout after 30000ms",
"affectedRegions": ["us-east-1", "eu-west-1"],
"duration": "12 mins, 4 secs"
}
}
ssl.expiring:
{
"event": "ssl.expiring",
"timestamp": "2024-01-15T00:00:00.000Z",
"monitor": {
"id": "m1aa2b3c4d5e6f",
"name": "API Production",
"url": "https://api.example.com"
},
"ssl": {
"daysUntilExpiry": 14,
"issue": "certificate_expiring",
"recommendation": "Renew the certificate before expiry and verify automatic renewal is active."
}
}
There is no expiresAt or issuer field in this payload. If you need
the certificate's absolute expiry timestamp, read sslExpiresAt for the
monitor from the Developer API instead of
trying to derive it from daysUntilExpiry. A webhook subscription's
ssl.expiring event, a different feature from this alert channel, does
carry data.ssl.expiresAt directly, as epoch milliseconds.
domain.expiring:
{
"event": "domain.expiring",
"timestamp": "2024-01-15T00:00:00.000Z",
"monitor": {
"id": "m1aa2b3c4d5e6f",
"name": "API Production",
"url": "https://api.example.com"
},
"domain": {
"name": "example.com",
"daysUntilExpiry": 14,
"issue": "registration_expiring",
"recommendation": "Renew the domain at the registrar, and check that auto-renew is on and the payment method on file is current."
}
}
An already-lapsed domain sends "issue": "registration_expired" with a
negative daysUntilExpiry instead. That's channel-only wording. A
webhook subscription's
domain.expiring event signals the same lapse with data.domain.milestone: 0.
Authentication
Pingara does not sign or authenticate outgoing webhook requests today.
There is no shared-secret header, no bearer token, and no way to configure
custom headers on a webhook channel. The only headers a delivery carries are
Content-Type: application/json and User-Agent: Pingara-Webhook/1.0, and
the latter is trivially spoofable, so don't rely on it as proof a request
came from Pingara.
The practical mitigation available today is to keep your endpoint URL private
and hard to guess, using a long random path segment rather than a predictable one
like /pingara-webhook. Anyone who has the URL can post to it, so treat it
like a credential: don't log it, commit it, or share it outside your team.
If verified webhook signing matters for your integration, email support@pingara.io. It isn't available yet, but we'd like to know it's blocking you.
Retry Behavior
If your endpoint fails in a way that might clear on its own, Pingara retries the webhook up to 3 times after the initial attempt (4 attempts total):
| Attempt | Delay after previous attempt |
|---|---|
| Initial attempt | - |
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry (final) | 15 minutes |
Not every failure is retried. Pingara classifies the response first, and only retries what could plausibly succeed later:
| Response | Retried? |
|---|---|
5xx (server error) | Yes |
408 (request timeout), 429 (rate limited) | Yes |
| Connection error, timeout, no response | Yes |
3xx (redirect) | No, a redirected webhook delivery is refused outright, never followed |
Any other 4xx (400, 401, 403, 404, …) | No, a bad URL, payload or credential won't fix itself in 21 minutes |
A non-retried failure is marked failed immediately, after a single attempt,
so if your endpoint answers 404 while you're deploying, you get one delivery
attempt and no more. After the final retry of a retried failure fails, the
notification is likewise marked as failed in the notification history.
Handling Retries
Your endpoint should be idempotent. Processing the same event twice should be safe. Use the incident.id and event fields to deduplicate:
const processedEvents = new Set();
app.post('/pingara-webhook', (req, res) => {
const eventKey = `${req.body.incident?.id}-${req.body.event}`;
if (processedEvents.has(eventKey)) {
return res.status(200).json({ status: 'already_processed' });
}
processedEvents.add(eventKey);
// Process the event...
res.status(200).json({ received: true });
});
Common Integration Patterns
Create a Ticket on Incident
app.post('/pingara-webhook', async (req, res) => {
if (req.body.event === 'incident.created') {
await ticketSystem.create({
title: `[Pingara] ${req.body.monitor.name} is down`,
description: req.body.incident.errorMessage,
priority: 'high',
tags: ['monitoring', 'auto-created'],
});
}
res.status(200).json({ received: true });
});
Post to a Custom Dashboard
app.post('/pingara-webhook', async (req, res) => {
await dashboard.addEvent({
source: 'pingara',
type: req.body.event,
monitor: req.body.monitor.name,
timestamp: req.body.timestamp,
errorType: req.body.incident?.errorType,
duration: req.body.incident?.duration,
});
res.status(200).json({ received: true });
});
Trigger an Automated Remediation
app.post('/pingara-webhook', async (req, res) => {
if (req.body.event === 'incident.created') {
// Automatically restart the service
await cloudProvider.restartService(req.body.monitor.name);
console.log('Auto-remediation triggered for', req.body.monitor.name);
}
res.status(200).json({ received: true });
});
Testing Webhooks
Using the Test Button
Pingara's Send Test button sends a realistic sample event to verify connectivity. The test payload uses dummy data but follows the exact same format as real events.
Using a Request Catcher
For development, use a request inspection tool:
- Go to webhook.site or requestbin.com
- Copy the unique URL
- Add it as a webhook endpoint in Pingara
- Send a test and inspect the full request
Local Development
Use a tunnel service like ngrok to test webhooks locally:
ngrok http 3000
# Use the generated https URL as your webhook endpoint
Troubleshooting
Webhook Not Firing
- Check alert policy - Is the webhook channel enabled?
- Check triggers - Is the event type enabled (Down, Recovery, etc.)?
- Check monitor link - Is the policy linked to the monitor?
Receiving 4xx/5xx Errors
- 401/403 - Authentication failed. Verify your secret/token.
- 404 - Endpoint URL is wrong. Check the path.
- 500 - Your server has a bug. Check server logs.
- Timeout - Your endpoint is too slow. Respond within 10 seconds.
Payload Parsing Errors
Ensure your endpoint:
- Sets
Content-Type: application/jsonmiddleware - Parses the JSON body correctly
- Handles missing optional fields gracefully
Next Steps
- Slack Integration - Pre-built Slack notifications
- Alert Channels - All available notification channels
- Setting Up Alerts - Configure alert policies
Related Articles
Slack Integration
Set up Slack webhook integration to receive rich, formatted Pingara alerts directly in your Slack channels with incident details and quick action links.
Alert Channels
Configure notification channels including Email, Slack, Microsoft Teams, Discord, webhooks, and PagerDuty to receive Pingara alerts wherever your team works.
Setting Up Alerts
Learn how to create alert policies, configure notification rules, and ensure your team is always informed when monitors detect issues.