Webhooks
Webhooks push real-time notifications to your server when things happen in Chronary — events created, updated, deleted, or agents modified. No polling required.
How it works
Section titled “How it works”- You register a webhook URL and choose which event types to listen for
- When a matching event occurs, Chronary sends an HTTP POST to your URL
- The request includes an HMAC-SHA256 signature for verification
- Failed deliveries are automatically retried with exponential backoff
Create a webhook
Section titled “Create a webhook”curl -X POST https://api.chronary.ai/v1/webhooks \ -H "Authorization: Bearer chr_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/chronary", "events": ["event.created", "event.updated", "event.deleted"] }'chronary webhooks create \ --url "https://your-server.com/webhooks/chronary" \ --events "event.created, event.updated, event.deleted"# Created webhook wh_a1b2c3d4# Secret: whsec_abc123...# (Save this secret — it won't be shown again)const response = await fetch('https://api.chronary.ai/v1/webhooks', { method: 'POST', headers: { 'Authorization': 'Bearer chr_sk_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ url: 'https://your-server.com/webhooks/chronary', events: ['event.created', 'event.updated', 'event.deleted'], }),});const webhook = await response.json();console.log(webhook.id); // "whk_a1b2c3"console.log(webhook.secret); // Save this — used for HMAC verificationfrom chronary import Chronary
client = Chronary()
webhook = client.webhooks.create( url="https://your-server.com/webhooks/chronary", events=["event.created", "event.updated", "event.deleted"],)print(webhook.secret) # Save this for HMAC verificationEvent types
Section titled “Event types”| Event type | Trigger |
|---|---|
event.created | A new event is added to any calendar |
event.updated | An event’s fields are modified |
event.deleted | An event is deleted |
agent.created | A new agent is created |
agent.updated | An agent’s fields are modified |
Webhook payload
Section titled “Webhook payload”Each delivery is an HTTP POST whose body is the raw event payload — there is no { id, type, timestamp, data } envelope. The event type travels in the X-Chronary-Event-Type header, the delivery ID in X-Delivery-Id, and the signing timestamp in X-Timestamp. For example, an event.created delivery body:
{ "calendar_id": "cal_x1y2z3", "event": { "id": "evt_m1n2o3", "calendarId": "cal_x1y2z3", "title": "Strategy sync with Acme Corp", "startTime": "2026-04-07T14:00:00Z", "endTime": "2026-04-07T14:30:00Z", "status": "confirmed" }}Each event type carries its own payload shape — see the event catalog in the API reference.
Verifying signatures
Section titled “Verifying signatures”Every delivery includes an X-Signature header — sha256=<hex>, an HMAC-SHA256 of `${X-Timestamp}.${rawBody}` keyed with your webhook secret — plus an X-Timestamp header (Unix epoch seconds). Verify the signature against the raw request body before trusting the payload, and reject deliveries whose X-Timestamp is older than ~5 minutes to defend against replay. The SDKs do both for you:
import { verifySignature } from '@chronary/sdk';
// Pass the RAW body string and the request headers.// Throws on a bad/missing signature or a stale timestamp.app.post('/webhooks/chronary', async (req, res) => { const rawBody = await req.text(); try { await verifySignature(rawBody, req.headers, process.env.WEBHOOK_SECRET); } catch { return res.status(401).send('Invalid signature'); } // ...handle the verified payload res.status(200).end();});from chronary.webhook import verify_signature, SignatureVerificationError
# In your Flask/FastAPI request handler:try: verify_signature( payload=request.body, # raw bytes/str headers=request.headers, # reads X-Signature + X-Timestamp secret=os.environ["WEBHOOK_SECRET"], )except SignatureVerificationError: return Response("Invalid signature", status=401)Retry behavior
Section titled “Retry behavior”If your endpoint returns a non-2xx status or times out (30 second timeout), Chronary retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | ~1 minute |
| 2nd retry | ~5 minutes |
| 3rd retry | ~30 minutes |
| 4th retry | ~2 hours |
| 5th retry | ~8 hours |
After 5 failed retries, the delivery is marked as failed. The webhook remains active for future events.
List webhooks
Section titled “List webhooks”curl https://api.chronary.ai/v1/webhooks \ -H "Authorization: Bearer chr_sk_your_key_here"Update a webhook
Section titled “Update a webhook”Change the URL, event types, or active status:
curl -X PATCH https://api.chronary.ai/v1/webhooks/whk_a1b2c3 \ -H "Authorization: Bearer chr_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "events": ["event.created", "event.updated"], "active": false }'Set active: false to temporarily pause deliveries without deleting the webhook.
Delete a webhook
Section titled “Delete a webhook”curl -X DELETE https://api.chronary.ai/v1/webhooks/whk_a1b2c3 \ -H "Authorization: Bearer chr_sk_your_key_here"Returns 204 No Content.
Best practices
Section titled “Best practices”- Always verify signatures — never trust a webhook payload without checking the HMAC
- Return 200 quickly — process the payload asynchronously if it takes more than a few seconds
- Handle duplicates — use the
X-Delivery-Idheader to deduplicate, as retries reuse the same delivery ID