Skip to content

Documentation · API

Calendar API

9 min read · REST reference · Updated 2026-05-21

REST9 minShare
Documentation sections

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.

MethodPath / eventPurpose
GET/v1/bookingsList bookings (paginated + filterable)
GET/v1/bookings/{id}Retrieve a single booking
POST/v1/bookingsCreate a booking (slot reservation)
PATCH/v1/bookings/{id}Modify attendees, notes, room, time
POST/v1/bookings/{id}/cancelCancel and trigger refund flow
GET/v1/availabilityFree/busy windows for a room or tier
GET/v1/calendar.icsiCal feed scoped to a token

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 of pending_payment, confirmed, cancelled, completed, expired.
  • room_id, member_id, company_id — UUIDs.
  • limit — 1–200. cursor — opaque string from previous response.
list_bookings.curl
bash
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'
list_bookings.node
ts
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
}
list_bookings.python
py
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)

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.
create_booking.curl
bash
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
  }'
create_booking.response
json
{
  "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 }
}

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

update_booking.curl
bash
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.

cancel_booking.curl
bash
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
  }'

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.

MethodPath / eventPurpose
EVTbooking.createdRow inserted at pending_payment. Pre-checkout.
EVTbooking.confirmedPayment succeeded. Slot is real money.
EVTbooking.updatedTime, room, attendees, or notes changed.
EVTbooking.cancelledUser or operator cancelled. Refund initiated.
EVTbooking.expiredCheckout window elapsed without payment.
EVTbooking.completedEnd-of-slot reached and no cancellation.
EVTbooking.no_showOperator marked attendee as no-show.
verify_signature.node
ts
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');
  }
);

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.

CodeHTTPMeaning
auth_invalid401Bearer token missing, malformed, or revoked.
auth_scope_insufficient403Token lacks calendar:write for a mutating call.
booking_not_found404Booking id doesn't exist in this workspace.
slot_unavailable409Another booking exists or room is in a blackout window.
slot_misaligned422start_at / end_at don't sit on the room's slot grid.
idempotency_conflict409Same idempotency_key reused with a different body.
rate_limited429Workspace bucket exhausted. Inspect retry_after_ms.
stripe_unconfigured412Workspace hasn't completed Stripe Connect onboarding.
internal_error500Server bug. We pick these up automatically via Sentry.