API and webhooks
Included with Pro. Create keys and webhooks under Developers in your dashboard.
Authentication
Send your key as a bearer token. Keys have full access to your account, so keep them on a server, never in a web page.
curl https://cushycal.com/api/v1/me \ -H "Authorization: Bearer bk_your_key_here"
Responses are JSON: { data, next? } on success, { error: { code, message } } otherwise. Each key can make 120 requests a minute (HTTP 429 with Retry-After beyond that). Times are ISO 8601 in UTC; money is in the currency's minor unit (cents).
Endpoints
| GET | /api/v1/me | The account the key belongs to (use it to test a connection). |
| GET | /api/v1/event-types | Your event types, with their booking links. |
| GET | /api/v1/slots?eventTypeId=…&start=…&end=… | Free start times (UTC) for an event type, up to 62 days at a time. |
| GET | /api/v1/bookings?status=upcoming&from=…&to=…&cursor=… | Your bookings, oldest first, 50 a page. status: upcoming, confirmed, pending, cancelled. |
| GET | /api/v1/bookings/{uid} | One booking. |
| POST | /api/v1/bookings | Book a client into one of your event types (same rules and emails as your booking page). Needs an Idempotency-Key header. |
| POST | /api/v1/bookings/{uid}/cancel | Cancel as the host: { reason?, notify? }. Anything paid is refunded in full. |
| GET | /api/v1/clients?q=…&tag=… | Your clients with their booking history and totals. |
Creating a booking:
curl -X POST https://cushycal.com/api/v1/bookings \
-H "Authorization: Bearer bk_your_key_here" \
-H "Idempotency-Key: order-1042" \
-H "Content-Type: application/json" \
-d '{
"eventTypeId": "…",
"start": "2026-11-03T09:00:00+08:00",
"name": "Priya Sharma",
"email": "priya@example.com",
"timeZone": "Europe/London"
}'Retrying with the same Idempotency-Key and body returns the same booking instead of making another. A taken time returns 409 slot_unavailable; form problems return 422 with fields.
Webhooks
We POST JSON to your URL within seconds of each event you subscribe to:
booking.created— New bookingbooking.requested— New booking waiting for your approvalbooking.approved— You approved a bookingbooking.declined— You declined a bookingbooking.expired— A request expired unansweredbooking.rescheduled— Booking movedbooking.cancelled— Booking cancelledpayment.received— Payment receivedpayment.refunded— Refund sentproposal.created— A client proposed a time
{
"id": "0192…", // the same on every retry: ignore ones you've seen
"type": "booking.created",
"createdAt": "2026-11-01T08:15:00.000Z",
"data": { "booking": { "uid": "…", "status": "confirmed", "start": "…", "attendees": [ … ], … } }
}Reply with any 2xx within 10 seconds. Otherwise we retry with backoff for about a day; after 15 events fail in a row the webhook is switched off and we email you.
Checking the signature
Each request has a Booking-Signature: t=…,v1=… header: an HMAC-SHA256, with your signing secret, of t + "." + raw body.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Discord webhooks get a short readable message instead, with no signature (Discord doesn't check them).