The Calendar API lets you list, create, modify, and cancel bookings on behalf of a workspace. Every endpoint here is REST + JSON, authenticated with a scoped bearer token, and idempotent on a client-provided idempotency_key.
If you're building an internal tool, syncing to a calendar (Google/Outlook), or powering a portal — this is your home page. For low-level slot availability and per-tier booking rules, see Booking rules.
Base URL. https://api.litehq.com/v1 — version is in the path, not a header. All endpoints below are /v1/... relative to that base.
Section 1
Endpoints overview
Reference · 7 endpoints
The Calendar API is small on purpose — seven endpoints cover every booking flow we've seen in two years of operator interviews. If you need something more bespoke, drop us a line; we'd rather extend a flag than ship endpoint #8.
| Method | Path / event | Purpose |
|---|---|---|
| GET | /v1/bookings | List bookings (paginated + filterable) |
| GET | /v1/bookings/{id} | Retrieve a single booking |
| POST | /v1/bookings | Create a booking (slot reservation) |
| PATCH | /v1/bookings/{id} | Modify attendees, notes, room, time |
| POST | /v1/bookings/{id}/cancel | Cancel and trigger refund flow |
| GET | /v1/availability | Free/busy windows for a room or tier |
| GET | /v1/calendar.ics | iCal feed scoped to a token |
calendar:read or calendar:write scope. Scoped keys are created from Operator dashboard → Settings → API keys. Two keys per workspace active at a time — rotate by creating a new one, swapping, then revoking the old.Section 2
Listing bookings
GET /v1/bookings
The list endpoint is cursor-paginated (50 per page by default, max 200). Filter by date range, status, room, member, or company. Responses include a next_cursortoken — null when you're at the end of the list.
Common query parameters
from— ISO 8601 datetime, inclusive. Defaults to now.to— ISO 8601 datetime, exclusive. Defaults to now + 30 days.status— one ofpending_payment,confirmed,cancelled,completed,expired.room_id,member_id,company_id— UUIDs.limit— 1–200.cursor— opaque string from previous response.
curl -G https://api.litehq.com/v1/bookings \
-H 'Authorization: Bearer sk_live_...' \
--data-urlencode 'from=2026-05-22T00:00:00Z' \
--data-urlencode 'to=2026-05-29T00:00:00Z' \
--data-urlencode 'status=confirmed' \
--data-urlencode 'limit=100'import { LiteHQ } from '@litehq/sdk';
const litehq = new LiteHQ({ apiKey: process.env.LITEHQ_API_KEY! });
const page = await litehq.bookings.list({
from: '2026-05-22T00:00:00Z',
to: '2026-05-29T00:00:00Z',
status: 'confirmed',
limit: 100,
});
for (const booking of page.data) {
console.log(booking.id, booking.room.name, booking.start_at);
}
if (page.next_cursor) {
// ... loop with cursor: page.next_cursor
}from litehq import LiteHQ
client = LiteHQ(api_key=os.environ['LITEHQ_API_KEY'])
for booking in client.bookings.list(
from_='2026-05-22T00:00:00Z',
to='2026-05-29T00:00:00Z',
status='confirmed',
).auto_paging():
print(booking.id, booking.room.name, booking.start_at)from=2026-05-22T00:00:00Z and to=2026-05-23T00:00:00Z.Section 3
Creating a booking
POST /v1/bookings
Creating a booking reserves the slot immediately at status=pending_paymentwhile the payer completes Stripe Checkout. The booking row exists before any charge, so the slot is held for the full checkout window (default 30 min, configurable). If payment doesn't land, an internal cron flips the row to expired and the slot frees up.
Required fields
room_id— UUID. Get this from/v1/rooms.start_at,end_at— ISO 8601 UTC, must align to the room's slot grid.booked_by— either{ member_id: "..." }or{ guest: { email, name } }.idempotency_key— client-generated UUID. Reuse to safely retry the same logical create.
curl -X POST https://api.litehq.com/v1/bookings \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 0e8a...-4d6f-...-bc14' \\
-d '{
"room_id": "rm_4QkX...",
"start_at": "2026-05-23T14:00:00Z",
"end_at": "2026-05-23T15:30:00Z",
"booked_by": { "member_id": "mem_J7P..." },
"notes": "Studio mic check 13:55",
"attendees": 4
}'{
"id": "bk_7Wq...",
"status": "pending_payment",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_b1...",
"expires_at": "2026-05-22T09:11:00Z",
"room": { "id": "rm_4QkX...", "name": "Studio" },
"start_at": "2026-05-23T14:00:00Z",
"end_at": "2026-05-23T15:30:00Z",
"price": { "currency": "GBP", "amount_minor": 8400 }
}checkout_urlis a Stripe Checkout Session URL. Redirect the user there — payment confirmation lands via webhook within ~5s of completion. Never store the embedded cs_* id in your own payment_intent_idcolumn — those are distinct domains. (Our DB rejects this via CHECK constraints, but it's the most common foot-gun in first-week integrations.)Section 4
Updating + cancelling
PATCH /v1/bookings/{id} · POST /v1/bookings/{id}/cancel
Bookings can be edited up to 1 hour before start_at by default; per-room rules can tighten or loosen this window. Editable fields: start_at, end_at, attendees, notes, and room_id (with a re-availability check).
Updating
curl -X PATCH https://api.litehq.com/v1/bookings/bk_7Wq... \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"end_at": "2026-05-23T16:00:00Z",
"attendees": 6,
"notes": "Extended by 30 min — client running over."
}'Lengthening a slot through PATCH triggers a delta-charge via the same Stripe Connect account as the original booking. A new charge.succeeded webhook fires with the booking id in metadata.
Cancelling
Cancellation is a POST — not a DELETE — because it changes state (refund, audit log, attendee notifications) rather than just removing a record.
curl -X POST https://api.litehq.com/v1/bookings/bk_7Wq.../cancel \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"reason": "member_requested",
"refund_policy": "full",
"notify_attendees": true
}'full, partial (specify refund_amount_minor), or none. Refunds are pushed immediately through Stripe; expect charge.refunded on your webhook within seconds.Section 5
Webhooks for booking events
POST → your endpoint · HMAC-SHA256 signed
Webhooks land on a single endpoint you register under Settings → Webhooks. Every event is signed with LiteHQ-Signature (HMAC-SHA256 of the raw body with your endpoint secret). Verify beforeparsing JSON — this is the standard pattern you'll have seen in Stripe's docs.
| Method | Path / event | Purpose |
|---|---|---|
| EVT | booking.created | Row inserted at pending_payment. Pre-checkout. |
| EVT | booking.confirmed | Payment succeeded. Slot is real money. |
| EVT | booking.updated | Time, room, attendees, or notes changed. |
| EVT | booking.cancelled | User or operator cancelled. Refund initiated. |
| EVT | booking.expired | Checkout window elapsed without payment. |
| EVT | booking.completed | End-of-slot reached and no cancellation. |
| EVT | booking.no_show | Operator marked attendee as no-show. |
import crypto from 'node:crypto';
import express from 'express';
app.post('/webhooks/litehq',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('LiteHQ-Signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.LITEHQ_WEBHOOK_SECRET!)
.update(req.body) // raw Buffer, not parsed JSON
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString());
// ... handle event.type
res.status(200).send('ok');
}
);event.id— duplicate deliveries are guaranteed eventually.Section 6
Rate limits + best practices
100 req/min · burst 500 · per workspace
The Calendar API allows 100 requests/min sustained per workspace, with a 500-request burst bucket that refills at the sustained rate. Limits are returned in headers on every response:
X-RateLimit-Limit— the bucket size.X-RateLimit-Remaining— tokens left in the current window.X-RateLimit-Reset— unix seconds when the bucket refills.
If you hit a 429
The response body includes a retry_after_msfield. Honour it — we track repeated 429-ignoring clients and will throttle the key down to 20 req/min on the second offence. The SDK clients all auto-respect this header.
Section 7
Common errors
Reference table · 9 codes you'll see in production
Errors return JSON with shape { error: { code, message, request_id } }.request_idis the trace identifier — quote it in support tickets.
| Code | HTTP | Meaning |
|---|---|---|
| auth_invalid | 401 | Bearer token missing, malformed, or revoked. |
| auth_scope_insufficient | 403 | Token lacks calendar:write for a mutating call. |
| booking_not_found | 404 | Booking id doesn't exist in this workspace. |
| slot_unavailable | 409 | Another booking exists or room is in a blackout window. |
| slot_misaligned | 422 | start_at / end_at don't sit on the room's slot grid. |
| idempotency_conflict | 409 | Same idempotency_key reused with a different body. |
| rate_limited | 429 | Workspace bucket exhausted. Inspect retry_after_ms. |
| stripe_unconfigured | 412 | Workspace hasn't completed Stripe Connect onboarding. |
| internal_error | 500 | Server bug. We pick these up automatically via Sentry. |
slot_unavailable and idempotency_conflict, and the correct retry behaviour differs. Always switch on error.code.Next steps
You’ve got bookings under control. Pick what’s next.
- Webhooks reference
Full event catalogue + signature verification examples in 4 languages.
- Booking rules
Slot grids, blackouts, per-tier permissions, surge pricing overrides.
- Billing API
Invoices, payments, refunds, and the Xero sync state machine.
- API authentication
Token rotation, scoped keys, OAuth callback for integrations.