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/meThe account the key belongs to (use it to test a connection).
GET/api/v1/event-typesYour 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/bookingsBook 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}/cancelCancel 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 booking
  • booking.requested — New booking waiting for your approval
  • booking.approved — You approved a booking
  • booking.declined — You declined a booking
  • booking.expired — A request expired unanswered
  • booking.rescheduled — Booking moved
  • booking.cancelled — Booking cancelled
  • payment.received — Payment received
  • payment.refunded — Refund sent
  • proposal.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).