Receive webhooks from Zyra
Chapter 2 · about 15 minutes to read
Polling the API works, but it wastes calls and adds latency. Webhooks invert the flow: Zyra POSTs to a URL you own whenever something interesting happens. This chapter covers subscription, signature verification, retries, and the dashboard's delivery history.
Time: about 15 minutes. Prerequisites: an HTTPS endpoint you control (ngrok or a real server). An Org Admin account.
Step 1 — Register your endpoint
Open Settings → Webhooks → New endpoint, or POST to the API:
curl -X POST https://app.getzyra.io/api/v1/webhooks/endpoints \
-H "Authorization: Bearer $ZYRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.acme.example/zyra",
"events": ["virtual_server.started", "virtual_server.stopped", "job.completed"],
"description": "prod ops bus"
}'
Zyra stores the URL, generates a 32-byte signing secret, and returns it once. Limits: 10 endpoints per organization.
The event catalogue
Events you can subscribe to today, in dot-notation:
device.online,device.offline,device.approved,device.removedvirtual_server.created,.started,.stopped,.terminated,.errorjob.queued,.started,.completed,.failedsla.breach.started,sla.breach.resolvedinvoice.generated,invoice.paid,payment.failed
A subscription only receives events on its events list. [VERIFY: full event enum path — today event_type is a free-form string per WebhookDelivery model]
Step 2 — Verify the signature
Every POST carries two headers: X-Zyra-Event (the event type) and X-Zyra-Signature (HMAC-SHA256 hex digest of the JSON body, signed with your endpoint secret). Always verify — an unsigned POST to your URL could come from anywhere.
Python
import hmac, hashlib
sent = req.headers["X-Zyra-Signature"]
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sent, expected):
raise HTTPException(401, "bad signature")
Node.js
const expected = crypto.createHmac("sha256", SECRET)
.update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))) {
return res.status(401).end();
}
Step 3 — Acknowledge fast
Return HTTP 2xx within 10 seconds. Anything else counts as a failure. If you need to do heavy work, push the event onto your own queue and return 200 immediately.
Retry policy
Failed deliveries retry with exponential backoff: 5 attempts, base 2 seconds. After 5 attempts the row is marked expired and dropped. Successive failures bump WebhookEndpoint.failure_count and stamp last_failure_at so you can spot a misbehaving consumer from the dashboard.
A nightly webhook_cleanup background job removes delivery rows older than 30 days.
Step 4 — Inspect delivery history
The dashboard ships a per-endpoint Deliveries tab backed by GET /api/v1/webhooks/endpoints/{id}/deliveries. Each row shows event type, attempt count, HTTP response status, the first 2 KB of the response body, and the final status (pending / delivered / failed / expired).
Step 5 — Test before going live
POST a synthetic event to confirm the receiver works:
curl -X POST https://app.getzyra.io/api/v1/webhooks/endpoints/$ID/test \
-H "Authorization: Bearer $ZYRA_API_KEY"
This dispatches a webhook.test event with a sample payload. If your URL responds 200, you're done.
What just happened
You registered an endpoint, verified signatures, learned the retry policy, and tested it end-to-end. Cross-link to Chapter 4: audit log if you also want a persistent record of every event.
Troubleshooting
- Endpoint shows
failure_count > 0but you got nothing. Check the Deliveries tab. Common cause: your signature verification rejected the request. - Events you expect aren't firing. Confirm the event name is on your endpoint's
eventsarray. - Localhost / 127.0.0.1 URLs are rejected. SSRF protection blocks loopback addresses. Use ngrok or a public URL in development.
Last reviewed: 2026-05-21