Authentication
Create a key in the panel (Settings โ API keys) and send it as a Bearer token. Keys are shown once and stored hashed. There are three kinds:
Full access โ read and write.
Read-only โ every GET endpoint.
Monitor-specific โ reads one monitor only; safe for public widgets.
Authorization: Bearer pl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Response format
JSON with snake_case keys. Timestamps are ISO 8601 in UTC; ids are public ULIDs. Single resources and lists are wrapped in data, with meta and links for pagination.
{
"data": {
"id": "01J8Z3ABCD1234EXAMPLE01",
"type": "http",
"name": "Production Website",
"url": "https://example.com",
"status": "up",
"interval_seconds": 60,
"created_at": "2026-09-28T10:00:00+00:00"
},
"meta": {
"next_cursor": "eyJpZCI6IjAxSjha..."
},
"links": {
"next": "https://panel.pingolab.net/api/v1/monitors?cursor=eyJpZCI6IjAxSjha..."
}
}
Errors
Errors always have the same shape. The code is stable and documented; the message follows your Accept-Language header (tr or en).
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"details": {
"url": [
"The url field must be a valid URL."
]
}
}
}
| Error Code | HTTP |
|---|---|
| unauthenticated | 401 |
| forbidden | 403 |
| not_found | 404 |
| method_not_allowed | 405 |
| idempotency_in_progress | 409 |
| validation_failed | 422 |
| idempotency_mismatch | 422 |
| rate_limited | 429 |
| server_error | 500 |
Pagination
Lists use cursor pagination: pass ?limit= (max 100, default 25) and follow links.next or meta.next_cursor via ?cursor=.
Rate limits
60 requests per minute per key by default. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; over the limit you get 429 with Retry-After.
Idempotency
Send an Idempotency-Key header with POST requests to retry safely: the first response is stored for 24 hours and replayed (Idempotent-Replayed: true). Reusing a key with a different body returns idempotency_mismatch.
Code examples
Make HTTP requests in your preferred programming language. All requests require a valid Bearer token.
# 1. List Monitors (GET)
curl https://panel.pingolab.net/api/v1/monitors \
-H "Authorization: Bearer pl_your_key" \
-H "Accept: application/json"
# 2. Create HTTP Monitor (POST)
curl -X POST https://panel.pingolab.net/api/v1/monitors \
-H "Authorization: Bearer pl_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f1c98a2-d4b3-4f9e-8c67-1234567890ab" \
-d '{
"type": "http",
"name": "Production Website",
"url": "https://example.com",
"interval_seconds": 60
}'
Webhooks
Webhook alert channels receive a JSON POST for every event. Each request is signed with HMAC-SHA256 over "timestamp.body" using the channel's signing secret. We retry up to 3 times with backoff.
X-Pingolab-Event: down
X-Pingolab-Timestamp: 1790335924
X-Pingolab-Signature: sha256=a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0
{
"event": "down",
"occurred_at": "2026-09-28T12:00:00+00:00",
"monitor": {
"id": "01J8Z3ABCD1234EXAMPLE01",
"name": "Production Website",
"type": "http",
"url": "https://example.com",
"status": "down"
},
"incident": {
"id": "01J8Z4XYZ9876INCIDENT01",
"started_at": "2026-09-28T11:59:00+00:00",
"resolved_at": null,
"duration_seconds": null,
"error_type": "timeout",
"error_message": "Connection timed out after 10000ms"
},
"details": {},
"links": {
"monitor": "https://panel.pingolab.net/monitors/01J8Z3ABCD1234EXAMPLE01"
}
}
Verifying the signature (constant-time)
Verify HMAC-SHA256 signatures to ensure webhook payloads originate from Pingolab and have not been tampered with. Always use constant-time string comparison to prevent timing attacks.
// PHP (HMAC-SHA256 Timing-Safe Verification)
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_PINGOLAB_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PINGOLAB_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
// Replay protection (5 minutes tolerance)
if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid webhook signature');
}
// Process event safely
$event = json_decode($payload, true);