Create a webhook
Give Chesly a public HTTPS URL, and it posts each new event there as JSON:
curl https://api.chesly.app/v1/webhooks \
-H "Authorization: Bearer $CHESLY_API_KEY" \
-H "Idempotency-Key: 6e9865aa-b31e-43b1-9a29-57e5a7ff4b6b" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/chesly", "event_types": ["message.received"]}'
The answer is the webhook with its secret, which starts with whsec_. It's shown only this once, so keep it with your server's other secrets. chesly webhooks create <url> does the same from the CLI.
event_typespicks the events to send. Leave it out for every event.networkspicks the networks. Leave it out for every network but email.payloadisfull, the default, orids.descriptionis a note for you.
You can have 25 webhooks on Personal and 100 on Pro, and deliveries never count as calls. The events are the ones in the catalog.
The request
Each delivery is a POST to your URL with Content-Type: application/json, User-Agent: Chesly-Webhooks/1.0, and the three Standard Webhooks headers:
| Header | Value |
|---|---|
webhook-id |
The event's ID, the same on every retry of it. Skip an ID you've already handled. |
webhook-timestamp |
When this attempt was signed, in Unix seconds |
webhook-signature |
v1, and the base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<body>, keyed with your secret. For 24 hours after a rotation there are two, separated by a space. |
POST /hooks/chesly HTTP/1.1
content-type: application/json
user-agent: Chesly-Webhooks/1.0
webhook-id: evt_LOBiK493MDUR1TH599lfcIHZ
webhook-timestamp: 1793095328
webhook-signature: v1,cE1wrSmMd09Me9szLF82KycDZ7wfRwR7xntFy6WOD/c=
The body is the event:
{
"id": "evt_LOBiK493MDUR1TH599lfcIHZ",
"type": "message.received",
"timestamp": "2026-10-27T10:02:07.518Z",
"data": {
"message": {
"id": "msg_UXw90uk2Q0SkBgQdA7uY.JGNXcGE1THJaY3JiMnFwRGIxM3ZjZXBaYS1tbjBEc2l3ZnA5d2Uxek9RVnc",
"chat_id": "chat_UXw90uk2Q0SkBgQdA7uY",
"sender": { "name": "Priya Patel", "is_me": false },
"text": "Got the deck, thank you! Looks great.",
"truncated": false,
"type": "text",
"media": null,
"sent_at": "2026-10-27T10:02:07.518Z",
"edited": false,
"deleted": false,
"reply_to": null,
"delivery": null
},
"chat": {
"id": "chat_UXw90uk2Q0SkBgQdA7uY",
"name": "Priya Patel",
"network": "slack",
"type": "dm"
}
}
}
With payload set to ids, data holds only the event's IDs, such as message_id, chat_id and account_id, and your code fetches the rest with its key. Use it when message text shouldn't pass through the service that receives your webhooks.
Verify the signature
Check every delivery before you trust it, against the raw body as it arrived: parse the JSON after. A Standard Webhooks library does the check, and refuses a timestamp more than 5 minutes off, so an old delivery can't be replayed.
In TypeScript, with npm install standardwebhooks:
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.CHESLY_WEBHOOK_SECRET!); // whsec_…
export async function POST(request: Request) {
const payload = await request.text();
let event;
try {
event = webhook.verify(payload, Object.fromEntries(request.headers));
} catch {
return new Response("Invalid signature", { status: 400 });
}
// Handle the event once per event.id, then answer quickly.
return new Response(null, { status: 204 });
}
In Python, with pip install standardwebhooks:
import os
from flask import Flask, request
from standardwebhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
webhook = Webhook(os.environ["CHESLY_WEBHOOK_SECRET"]) # whsec_…
@app.post("/hooks/chesly")
def chesly_webhook():
try:
event = webhook.verify(request.get_data(), dict(request.headers))
except WebhookVerificationError:
return "Invalid signature", 400
# Handle the event once per event["id"], then answer quickly.
return "", 204
Retries
Any 2xx answer is a success. Anything else is a failure: another status, no answer within 15 seconds, or a redirect, which Chesly never follows. After a failure, Chesly tries again in 5 seconds, then 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours: 8 attempts over about 28 hours. Waits of 5 minutes or more vary by up to 10%. When you answer 429, 502, 503 or 504 with a longer Retry-After, Chesly waits that long instead, up to 10 hours.
Answer within 15 seconds and do slow work afterwards. A retried event can arrive after newer ones, so order events by their timestamp, and skip a webhook-id you've already handled.
When a webhook turns off
A webhook that's off has status: disabled, and disabled_reason says why:
gone: it answered410 Gone, which turns it off at once.failing: it kept failing for 3 days. Any success in between starts the count again.user: you turned it off, withPATCH /v1/webhooks/{id}and{"enabled": false}, or in the dashboard.key_revoked: an API key that made it, changed it or set its URL was revoked because it leaked, so it was turned off with the key. It comes back on only in the dashboard: an API key that tries gets403forbidden.
While a webhook is off, its pending deliveries are dropped. Turn it back on with PATCH /v1/webhooks/{id} and {"enabled": true}, or in the dashboard. While your account has no API plan, deliveries are dropped instead of sent.
Where a webhook can point
https://only, in at most 2,048 characters, with no user name or password in the URL.- The host must not resolve to a private, loopback, link-local, shared (
100.64.0.0/10), multicast or cloud metadata address. Chesly checks when you create the webhook, and again on every delivery, against the address it connects to. - Redirects are never followed.
Rotate the secret
POST /v1/webhooks/{id}/rotate_secret, or chesly webhooks rotate <id>, answers with a new secret and previous_secret_expires_at. For the next 24 hours, each delivery is signed with both secrets, so you can switch your server to the new one without missing a delivery. A Standard Webhooks library accepts a delivery when any one of its signatures matches.
Test and debug
POST /v1/webhooks/{id}/test, orchesly webhooks test <id>, sends a test delivery now. Itswebhook-idstarts withevt_test_.GET /v1/webhooks/{id}/deliveries, orchesly webhooks deliveries <id>, lists the webhook's recent deliveries: each one's status, attempts, last status code and the first 1,024 characters of your answer. The dashboard shows the same log.