Developers
Take bookings from your own website.
A small REST API for reading availability and writing bookings. Everything goes through the same engine the app itself uses, so the API cannot create a booking the board would have refused.
https://calendula.crawlink.dev/api/v1Getting a key
In the app, go to Settings → API keys and create one. Pick the narrowest set of scopes that does the job, and give each integration its own key so one can be revoked without breaking the others.
The key is shown once. Only its SHA-256 is stored, so nobody — including us — can recover it afterwards. Lose it and you revoke it and issue another.
Authentication
Send the key as a bearer token. X-Api-Key works too, for clients
that cannot set an Authorization header.
Authorization: Bearer ck_<organization>_<secret> The organization id is part of the key. That is what lets a request find your data without you having to send an account identifier alongside it.
Scopes
calendars.read Read calendars —
List calendars, services and locations.availability.read Read availability —
Ask which times are free on a given day.bookings.read Read bookings —
List and read bookings, including customer contact details.bookings.write Create and change bookings —
Take new bookings and move existing ones.bookings.cancel Cancel bookings —
Cancel a booking. Separate from writing, because it is destructive.bookings.override Book outside opening hours —
Ignore opening hours and slot boundaries. Only for systems recording walk-ins.customers.read Read customers —
List and read the customer directory.customers.write Create and change customers —
Add customers and edit their details.Endpoints
/health calendars.readConfirms the key works and reports which organization it belongs to.
/calendars calendars.readCalendars and locations, with the slot length, buffers and notice each one enforces.
/services calendars.readServices, their durations and prices, and which calendars deliver them.
/availability availability.readFree times for one calendar. `days` walks forward, so a month view is one call.
/bookings bookings.readBookings in a date range, filterable by calendar and status.
/bookings bookings.writeTakes a booking through the same availability engine the app uses.
/bookings/{id} bookings.readOne booking.
/bookings/{id} bookings.writeMoves a booking, edits its details, or changes its status.
/bookings/{id} bookings.cancelCancels. The record is kept.
/customers customers.readThe customer directory.
/customers customers.writeCreates a customer, or updates one when `id` is given.
Finding free times
curl "https://calendula.crawlink.dev/api/v1/availability?calendarId=CAL_ID&date=2026-09-01&days=7" \
-H "Authorization: Bearer ck_ORG_SECRET" Each day comes back with its open windows and the individual starts on offer. Every slot carries
both a local time and an absolute startsAt instant, so a caller in
another timezone never has to redo the conversion.
Taking a booking
curl -X POST "https://calendula.crawlink.dev/api/v1/bookings" \
-H "Authorization: Bearer ck_ORG_SECRET" \
-H "Content-Type: application/json" \
-d '{
"calendarId": "CAL_ID",
"serviceId": "SERVICE_ID",
"date": "2026-09-01",
"start": "18:30",
"customerName": "Asha Menon",
"customerPhone": "+919876543210"
}' start takes either "18:30" or
minutes from midnight. If the slot has gone since you read the availability, the response is 409 with slot_unavailable — re-read availability and offer the customer another
time rather than retrying the same one.
Errors
Every failure is JSON with a stable error.code — branch on the code,
not the message, which may be reworded.
{
"error": {
"code": "slot_unavailable",
"message": "That time is already booked. Please pick another time."
}
} missing_key 401 — no key was sentinvalid_key 401 — unknown, revoked or expiredmissing_scope 403 — the key lacks the scope for this callout_of_scope 403 — the key is limited to other calendarsnot_found 404 — no such booking or calendarslot_unavailable 409 — the time is no longer freerate_limited 429 — too many requests this minuteinvalid_body 400 — the request body was malformedRate limit
120 requests a minute per key by default. It exists to stop a runaway script, not to meter usage — if a legitimate integration needs more, ask and we will raise it.