Webhook events
Webhooks let you receive real-time notifications when events happen in your account — a snapshot completes, a monitor changes, or a visual diff is generated. Instead of polling the API, register a URL and we'll send a POST request to it whenever a subscribed event occurs.
Each webhook delivery is signed with HMAC-SHA256, includes automatic retries on failure, and comes with a delivery log so you can debug integration issues. You can register up to 5 webhook endpoints per account.
List webhook endpoints
Returns all your registered webhook endpoints with their subscribed events, active status, and delivery count.
/v1/webhookscurl "https://api.snapshotarchive.com/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"$response = Http::withToken('YOUR_API_KEY')
->get('https://api.snapshotarchive.com/v1/webhooks');
$webhooks = $response->json('data');resp = requests.get('https://api.snapshotarchive.com/v1/webhooks',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'})
webhooks = resp.json()['data']const resp = await fetch('https://api.snapshotarchive.com/v1/webhooks', {
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json' }
});
const { data: webhooks } = await resp.json();uri = URI('https://api.snapshotarchive.com/v1/webhooks')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Accept'] = 'application/json'
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
webhooks = JSON.parse(resp.body)['data']req, _ := http.NewRequest("GET", "https://api.snapshotarchive.com/v1/webhooks", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Response
{
"data": [
{
"id": 1,
"url": "https://example.com/webhook",
"events": ["snapshot.completed", "snapshot.failed"],
"description": "Notify on captures",
"is_active": true,
"last_triggered_at": "2026-10-01T12:00:00Z",
"deliveries_count": 47,
"created_at": "2026-09-15T10:00:00Z"
}
]
}Create a webhook endpoint
Registers a new URL to receive event notifications. The response includes a secret key used to verify webhook signatures — store it securely, it's only shown once. If you don't specify which events to subscribe to, the endpoint receives all events.
/v1/webhooksRequest body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL (max 2048 chars) |
events | array | No | Event types to subscribe to (see below) |
description | string | No | Description (max 255 chars) |
Available event types
| Event | Description |
|---|---|
snapshot.completed | A snapshot was captured successfully |
snapshot.failed | A snapshot capture failed |
monitor.created | A new monitor was created |
monitor.updated | A monitor's settings were changed |
monitor.deleted | A monitor was deleted |
monitor.paused | A monitor was paused |
monitor.resumed | A monitor was resumed |
diff.completed | A visual diff was generated |
curl -X POST https://api.snapshotarchive.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"url": "https://example.com/webhook",
"events": ["snapshot.completed", "snapshot.failed", "diff.completed"],
"description": "Capture notifications"
}'$response = Http::withToken('YOUR_API_KEY')
->post('https://api.snapshotarchive.com/v1/webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['snapshot.completed', 'snapshot.failed', 'diff.completed'],
'description' => 'Capture notifications',
]);
$webhook = $response->json('data');resp = requests.post('https://api.snapshotarchive.com/v1/webhooks',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'},
json={'url': 'https://example.com/webhook',
'events': ['snapshot.completed', 'snapshot.failed', 'diff.completed'],
'description': 'Capture notifications'})
webhook = resp.json()['data']const resp = await fetch('https://api.snapshotarchive.com/v1/webhooks', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
url: 'https://example.com/webhook',
events: ['snapshot.completed', 'snapshot.failed', 'diff.completed'],
description: 'Capture notifications',
}),
});
const { data: webhook } = await resp.json();uri = URI('https://api.snapshotarchive.com/v1/webhooks')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Content-Type'] = 'application/json'
req['Accept'] = 'application/json'
req.body = { url: 'https://example.com/webhook',
events: ['snapshot.completed', 'snapshot.failed', 'diff.completed'],
description: 'Capture notifications' }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
webhook = JSON.parse(resp.body)['data']body := strings.NewReader(`{"url":"https://example.com/webhook",
"events":["snapshot.completed","snapshot.failed","diff.completed"],
"description":"Capture notifications"}`)
req, _ := http.NewRequest("POST", "https://api.snapshotarchive.com/v1/webhooks", body)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Response
{
"data": {
"endpoint": {
"id": 1,
"url": "https://example.com/webhook",
"events": ["snapshot.completed", "snapshot.failed", "diff.completed"],
"is_active": true
},
"secret": "a1b2c3d4e5f6..."
}
}secret is only returned when the endpoint is created. Use it to verify the X-Webhook-Signature header on incoming deliveries (HMAC-SHA256 of the payload body).
Update and delete a webhook
Update the URL, subscribed events, description, or active status.
# Pause a webhook
curl -X PUT https://api.snapshotarchive.com/v1/webhooks/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"is_active": false}'// Pause a webhook
$response = Http::withToken('YOUR_API_KEY')
->put('https://api.snapshotarchive.com/v1/webhooks/1', [
'is_active' => false,
]);
$webhook = $response->json('data');# Pause a webhook
resp = requests.put('https://api.snapshotarchive.com/v1/webhooks/1',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'},
json={'is_active': False})
webhook = resp.json()['data']// Pause a webhook
const resp = await fetch('https://api.snapshotarchive.com/v1/webhooks/1', {
method: 'PUT',
headers: { 'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ is_active: false }),
});
const { data: webhook } = await resp.json();# Pause a webhook
uri = URI('https://api.snapshotarchive.com/v1/webhooks/1')
req = Net::HTTP::Put.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Content-Type'] = 'application/json'
req['Accept'] = 'application/json'
req.body = { is_active: false }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
webhook = JSON.parse(resp.body)['data']// Pause a webhook
body := strings.NewReader(`{"is_active":false}`)
req, _ := http.NewRequest("PUT", "https://api.snapshotarchive.com/v1/webhooks/1", body)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Delete a webhook endpoint. Returns 204 No Content.
curl -X DELETE https://api.snapshotarchive.com/v1/webhooks/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"$response = Http::withToken('YOUR_API_KEY')
->delete('https://api.snapshotarchive.com/v1/webhooks/1');
// 204 No Contentresponse = requests.delete(
'https://api.snapshotarchive.com/v1/webhooks/1',
headers={'Authorization': 'Bearer YOUR_API_KEY'}
) # 204 No Contentconst response = await fetch('https://api.snapshotarchive.com/v1/webhooks/1', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
}); // 204 No Contenturi = URI('https://api.snapshotarchive.com/v1/webhooks/1')
req = Net::HTTP::Delete.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
# 204 No Contentreq, _ := http.NewRequest("DELETE", "https://api.snapshotarchive.com/v1/webhooks/1", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
// 204 No ContentRotate webhook secret
Generates a new signing secret for a webhook endpoint. The old secret stops working immediately, so update your receiver before rotating. This is useful if you suspect the secret has been compromised.
/v1/webhooks/{id}/rotate-secretcurl -X POST https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
# Response: {"data": {"secret": "new-secret-here..."}}$response = Http::withToken('YOUR_API_KEY')
->post('https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret');
$newSecret = $response->json('data.secret');resp = requests.post('https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'})
new_secret = resp.json()['data']['secret']const resp = await fetch('https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json' }
});
const { data: { secret } } = await resp.json();uri = URI('https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Accept'] = 'application/json'
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
new_secret = JSON.parse(resp.body).dig('data', 'secret')req, _ := http.NewRequest("POST", "https://api.snapshotarchive.com/v1/webhooks/1/rotate-secret", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Test webhook and view deliveries
Send a test ping to verify your endpoint is reachable and processing payloads correctly. You can also view the delivery history to debug failed deliveries and see response codes.
Sends a test payload to the endpoint. Returns the HTTP status code and response body from your server.
curl -X POST https://api.snapshotarchive.com/v1/webhooks/1/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
# Response: {"data": {"message": "Test webhook delivered successfully.", "status": 200}}$response = Http::withToken('YOUR_API_KEY')
->post('https://api.snapshotarchive.com/v1/webhooks/1/test');
$result = $response->json('data'); // message + statusresp = requests.post('https://api.snapshotarchive.com/v1/webhooks/1/test',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'})
result = resp.json()['data'] # message + statusconst resp = await fetch('https://api.snapshotarchive.com/v1/webhooks/1/test', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json' }
});
const { data: result } = await resp.json();uri = URI('https://api.snapshotarchive.com/v1/webhooks/1/test')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Accept'] = 'application/json'
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
result = JSON.parse(resp.body)['data']req, _ := http.NewRequest("POST", "https://api.snapshotarchive.com/v1/webhooks/1/test", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()List delivery history with status codes, response bodies, and retry information.
Query parameters
| Parameter | Type | Description |
|---|---|---|
event_type | string | Filter by event type |
delivered | boolean | true for successful, false for failed/pending |
per_page | integer | Items per page (default: 20) |
# View failed deliveries
curl "https://api.snapshotarchive.com/v1/webhooks/1/deliveries?delivered=false" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"$response = Http::withToken('YOUR_API_KEY')
->get('https://api.snapshotarchive.com/v1/webhooks/1/deliveries', [
'delivered' => false,
]);
$deliveries = $response->json('data');resp = requests.get('https://api.snapshotarchive.com/v1/webhooks/1/deliveries',
headers={'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json'},
params={'delivered': 'false'})
deliveries = resp.json()['data']const resp = await fetch('https://api.snapshotarchive.com/v1/webhooks/1/deliveries?delivered=false', {
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json' }
});
const { data: deliveries } = await resp.json();uri = URI('https://api.snapshotarchive.com/v1/webhooks/1/deliveries?delivered=false')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer YOUR_API_KEY'
req['Accept'] = 'application/json'
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
deliveries = JSON.parse(resp.body)['data']req, _ := http.NewRequest("GET", "https://api.snapshotarchive.com/v1/webhooks/1/deliveries?delivered=false", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Webhook payload structure
Every webhook delivery sends a JSON POST request with this structure:
{
"event": "snapshot.completed",
"timestamp": "2026-10-01T12:00:00Z",
"data": {
"snapshot_id": "9e8f7a6b-...",
"monitor_id": 42,
"url": "https://example.com",
"status": "completed"
}
}Verify the X-Webhook-Signature header by computing HMAC-SHA256 of the raw request body using your endpoint's secret key.