Events
Webhooks
Set up an endpoint
- 1
Expose a public HTTPS URL
It must accept
POSTrequests with a JSON body and answer with a 2xx status within 10 seconds. Do slow work after responding. Private and internal addresses (localhost, 10.x, 192.168.x, cloud metadata) are rejected. - 2
Subscribe it
In the dashboard under Integrations → Webhooks, or with the API (scope
manage:webhooks):Requestcurl -X POST https://calendeur.com/api/v1/webhooks \ -H "Authorization: Bearer $CALENDEUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://api.example.com/calendeur","events":["booking.created","booking.cancelled"]}'Store the
secretfrom the response; it's shown only once. - 3
Verify every delivery
Check the signature before trusting a payload. Anyone can send a request to your URL; only Calendeur knows the secret.
Payload
Each delivery is a POST with these headers and body:
Content-Type: application/json
User-Agent: Calendeur-Webhooks/1.0
X-Calendeur-Event: booking.created
X-Calendeur-Signature: sha256=5d1f0c...{
"event": "booking.created",
"timestamp": "2026-10-05T18:22:05.000Z",
"data": {
"bookingId": "bkg_8f2k1q",
"eventTypeId": "evt_30min",
"guestName": "Alex Morgan",
"guestEmail": "alex@example.com",
"guestTimezone": "America/Chicago",
"startTime": "2026-10-12T15:00:00.000Z",
"endTime": "2026-10-12T15:30:00.000Z",
"duration": 30,
"status": "UPCOMING"
}
}Verifying signatures
X-Calendeur-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your endpoint's secret. Compute it over the exact bytes received (before JSON parsing) and compare in constant time.
import crypto from "crypto"
import express from "express"
const app = express()
// Use the raw body: the signature covers the exact bytes we sent.
app.post("/calendeur", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.CALENDEUR_WEBHOOK_SECRET)
.update(req.body)
.digest("hex")
const given = req.get("X-Calendeur-Signature") || ""
const valid = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
if (!valid) return res.sendStatus(401)
const { event, data } = JSON.parse(req.body.toString())
if (event === "booking.created") {
// e.g. create the appointment in your own database
}
res.sendStatus(200)
})Handle duplicates
Make your handler idempotent: usedata.bookingId plus event as a key so processing the same delivery twice has no extra effect.Events
booking.created | A meeting was booked, from any channel. |
booking.rescheduled | A meeting moved to a new time. |
booking.cancelled | A meeting was cancelled by the host or guest. |
booking.completed | A meeting was marked completed. |
booking.no_show | The guest was marked as a no-show. |
booking.paid | Payment for a paid booking was received. |
meeting.notes_added | Notes or a transcript were added and summarized. |
routing.submitted | Someone submitted a routing form. |
document.sent | A document was sent for signature. |
document.signed | A signer signed a document. |
document.completed | Everyone has signed a document. |
document.declined | A signer declined a document. |
voice.call.started | An AI voice call started. |
voice.call.completed | An AI voice call ended. |
voice.call.meeting_booked | An AI voice call booked a meeting. |
voice.campaign.completed | An AI voice campaign finished. |
Manage subscriptions with the Webhooks API.