OhMyDesk Booking API
Read a space's live availability and create desk, meeting-room, whole-day and private-office
bookings straight from your own website, widget, or automations. Served from
ohmydesk.app — no backend URLs to wire up.
Base URL
https://ohmydesk.app/api/v1 Format
JSON, envelope { data, error, meta }
Auth
Public endpoints need no key; the rest use a per-space API key
Authentication
There are two tiers. Public endpoints — availability and desk bookings for a space — take no key and are safe to call from a browser or a public website, since they expose only what a space already publishes on its booking page. Everything else is authenticated with a per-organization API key.
Create and revoke keys in Settings → Integrations → API (owners and admins).
Send the key as a bearer token — or the x-api-key header:
Authorization: Bearer omd_live_… Get public availability
GET/public/{slug}/availability
Returns the space's desks (grouped by room), meeting rooms, private offices, and the currently
booked slots. slug is the space's public booking slug — the last segment of its
/book/<slug> page. This is the same read the public booking page and the
embeddable widget use.
curl https://ohmydesk.app/api/v1/public/your-space/availability
The data object contains org, rooms[] (each with
desks[] carrying deskId and label),
meetingRooms[], privateOffices[], and bookedSlots[]
(each { deskId, date }).
Create a desk booking
POST/public/{slug}/bookings
Books a free desk for each requested date. A desk is auto-assigned per date from live availability — you don't pick a specific desk. The space's operator is notified (and the visitor too, if you include an email), exactly as if the booking came through the space's own page.
curl -X POST https://ohmydesk.app/api/v1/public/your-space/bookings \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-2026-07-20-ada" \
-d '{
"dates": ["2026-07-20", "2026-07-21"],
"visitorName": "Ada Lovelace",
"visitorEmail": "ada@example.com",
"visitorPhone": "+359 888 123 456",
"notes": "Prefers a quiet corner"
}' Body
dates— array ofYYYY-MM-DD, at least one, each within the space's window. requiredvisitorName— who the booking is for. requiredvisitorEmail— optional; when present the visitor gets a confirmation email.visitorPhone— optional.notes— optional free-text note for the operator.
Response — 201 Created
{
"data": {
"bookings": [
{ "date": "2026-07-20", "deskId": "room2-desk4", "deskLabel": "Desk 8" },
{ "date": "2026-07-21", "deskId": "room1-desk2", "deskLabel": "Desk 2" }
]
},
"error": null,
"meta": { "slug": "your-space" }
} Book a meeting room (hourly)
POST/public/{slug}/meeting-room-bookings
Books a meeting room for a time slot on one day. Times are minutes from midnight
(600 = 10:00, 660 = 11:00), aligned to the room's slot size and within its opening hours — read
the room's slotMinutes, openMinute and closeMinute from
availability. An email is required. Pass companyName / companyAddress /
companyVatId for a B2B booking (a draft invoice is created).
curl -X POST https://ohmydesk.app/api/v1/public/your-space/meeting-room-bookings \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mr-2026-07-20-ada" \
-d '{
"meetingRoomId": "1ef38266-7b46-415d-b6f4-690c7673e382",
"date": "2026-07-20",
"startMinute": 600,
"endMinute": 660,
"visitorName": "Ada Lovelace",
"visitorEmail": "ada@example.com"
}' Response — 201 Created
{
"data": {
"booking": {
"success": true,
"booking_id": "…",
"price": 40,
"currency": "EUR",
"duration_minutes": 60,
"start_time": "2026-07-20T07:00:00+00:00",
"end_time": "2026-07-20T08:00:00+00:00"
}
},
"error": null,
"meta": { "slug": "your-space" }
} Book a room for whole days
POST/public/{slug}/whole-day-bookings
Books a whole-day-enabled room across a date range (startDate…endDate,
max 60 days). When the space books whole-day rooms for free, this returns 201 with a
booking_group_id. When the space takes card payment online, it
returns 200 with a checkoutUrl instead (a visitorEmail is
required) — see Paid resources.
curl -X POST https://ohmydesk.app/api/v1/public/your-space/whole-day-bookings \
-H "Content-Type: application/json" \
-d '{
"meetingRoomId": "…",
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"visitorName": "Ada Lovelace",
"visitorEmail": "ada@example.com"
}' Request a private office
POST/public/{slug}/office-requests
Registers interest in a private office — normally this creates a lead for the operator to follow
up, not a confirmed booking (202 Accepted, data.received: true).
companyName is required. When the space takes card payment online for
an available office, it returns 200 with a checkoutUrl instead (an
email is required, and paying the first month reserves the office) — see
Paid resources.
curl -X POST https://ohmydesk.app/api/v1/public/your-space/office-requests \
-H "Content-Type: application/json" \
-d '{
"officeId": "…",
"companyName": "Difference Engine Ltd",
"email": "ada@example.com",
"phone": "+359 888 123 456",
"preferredStart": "2026-08-01",
"message": "Team of 4, need parking",
"returnUrl": "https://your-site.com/booking/done"
}' Paid resources
Whole-day rooms and private offices that a space has set to take card payment online
return 200 with a Stripe checkoutUrl instead of confirming immediately.
Redirect the buyer there; once they pay, OhMyDesk creates the booking (or opens the office agreement)
and notifies the operator, exactly as a card booking on the space's own page would. The response
carries paymentRequired: true so you can branch on it:
{
"data": {
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_…",
"paymentRequired": true
},
"error": null,
"meta": { "slug": "your-space" }
} - These paths require an email (
visitorEmailfor whole-day,emailfor offices) — Stripe needs it for the receipt. - Add an optional
returnUrlto send the buyer back to your own page afterward; OhMyDesk appends?payment=success&session_id=…(or?payment=cancelled). Omit it and the buyer returns to the space's hosted booking page. - Sending the same
Idempotency-Keyagain replays the samecheckoutUrl— it won't mint a second checkout session.
Hourly meeting-room bookings and desk bookings are always created directly (no checkout).
Idempotency
Send an Idempotency-Key header with any booking POST to make it safe
to retry. The first request with a given key does the work and stores its result; any later
request with the same key replays that stored response instead of creating a second
booking. Use a fresh, unique key for each distinct booking attempt (a UUID works well).
Without an Idempotency-Key, a repeated POST creates a new booking
each time — so always send one if the caller might retry.
Your organization (authenticated)
GET/ API key
Confirms your API key and returns the organization it belongs to — a handy smoke test.
curl https://ohmydesk.app/api/v1 \
-H "Authorization: Bearer omd_live_…" GET/availability API key
Your own organization's availability, in the same shape as the public read above.
List your bookings (authenticated)
GET/bookings API key
Lists your organization's desk and meeting-room bookings in a date window. Frozen desk days
and cancelled meeting-room bookings are left out. Every row carries a type
(desk or room) and the id you pass to cancel.
curl "https://ohmydesk.app/api/v1/bookings?from=2026-07-20&to=2026-08-20&type=all" \
-H "Authorization: Bearer omd_live_…" Query
from/to—YYYY-MM-DDwindow matched on each booking's date (default: today … today + 30 days; span capped at 366 days).type—desk,room, orall(defaultall).
Response — 200 OK (rows sorted by date; fields abbreviated below)
{
"data": {
"bookings": [
{
"type": "desk",
"id": "720307628",
"deskId": "room2-desk4",
"date": "2026-07-20",
"status": "assigned",
"personName": "Ada Lovelace",
"isFlex": false,
"clientId": "1745"
},
{
"type": "room",
"id": "1ef38266-7b46-415d-b6f4-690c7673e382",
"meetingRoomId": "c838f5dd-…",
"date": "2026-07-20",
"startTime": "2026-07-20T09:00:00+00:00",
"endTime": "2026-07-20T10:00:00+00:00",
"status": "booked",
"bookingGroupId": null,
"isFullDay": false
}
],
"range": { "from": "2026-07-20", "to": "2026-08-20" }
},
"error": null,
"meta": { "organization_id": "…" }
}
A multi-day desk booking shows up as one row per day, each with its own id —
cancelling one of those ids cancels just that day.
Cancel a booking (authenticated)
POST/bookings/{id}/cancel API key
Cancels a booking your organization owns. The id comes from
list bookings: a numeric id cancels a desk booking
(removed — and if it was a flex day, the member's flex allowance is refunded, shown by
flexRestored); a UUID cancels a meeting-room booking (marked
cancelled). Cancelling one day of a whole-day reservation cancels the whole group, and
rowsCancelled reports how many rows were affected. An id that doesn't belong to your
organization returns booking_not_found — never touching another space's data.
curl -X POST https://ohmydesk.app/api/v1/bookings/720307628/cancel \
-H "Authorization: Bearer omd_live_…" \
-H "Idempotency-Key: cancel-720307628" Response — 200 OK
{
"data": {
"cancelled": true,
"type": "desk",
"id": "720307628",
"deskId": "room2-desk4",
"date": "2026-07-20",
"flexRestored": false
},
"error": null,
"meta": { "organization_id": "…" }
}
Re-cancelling an already-cancelled meeting room returns 200 with
already: true. Send an Idempotency-Key to make a cancel safe to retry.
Smart-lock access codes are not yet revoked automatically on cancel.
Edit or reschedule a booking (authenticated)
PATCH/bookings/{id} API key
Edits or reschedules a booking your organization owns — a meeting-room booking
(the id is a room UUID) or a single-day desk booking (the
id is a numeric desk-booking id), both from list bookings.
Send only the fields you want to change — everything else is left as-is.
Meeting-room booking
To reschedule, send date and/or startMinute/endMinute
(minutes from midnight, like the create endpoints); the new time is validated against the
room's opening hours, slot size, and min/max length, and re-checked for conflicts (a clash
returns slot_taken).
curl -X PATCH https://ohmydesk.app/api/v1/bookings/1ef38266-7b46-415d-b6f4-690c7673e382 \
-H "Authorization: Bearer omd_live_…" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-07-21",
"startMinute": 660,
"endMinute": 780,
"personName": "Ada Lovelace",
"status": "confirmed"
}' Body — all optional; at least one required
date,startMinute,endMinute— reschedule. Omitting one side of the time keeps the current value.personName,title,notes— details.status—bookedorconfirmed(use cancel to cancel).price— override the price. If you reschedule without sending a price, it's recomputed from the room's hourly rate.companyName,companyAddress,companyVatId— B2B details.
Response — 200 OK
{
"data": {
"updated": true,
"id": "1ef38266-7b46-415d-b6f4-690c7673e382",
"type": "room",
"meetingRoomId": "…",
"date": "2026-07-21",
"startTime": "2026-07-21T08:00:00+00:00",
"endTime": "2026-07-21T10:00:00+00:00",
"status": "confirmed",
"price": 50,
"currency": "EUR"
},
"error": null,
"meta": { "organization_id": "…" }
} Desk booking
To reschedule, send date (YYYY-MM-DD) and/or deskId
(a desk id from availability). The move is applied in place — the
booking keeps its id — and the target desk + date is re-checked for conflicts (a clash returns
slot_taken). The id stays stable, so any smart-lock grants and invoice
lines linked to it are preserved.
curl -X PATCH https://ohmydesk.app/api/v1/bookings/21808 \
-H "Authorization: Bearer omd_live_…" \
-H "Content-Type: application/json" \
-d '{
"deskId": "room2-desk4",
"date": "2026-07-21",
"personName": "Ada Lovelace",
"status": "assigned"
}' Body — all optional; at least one required
date,deskId— reschedule to another day and/or desk.personName,title,notes— details.visitorName,visitorPhone— visitor contact details.status—bookedorassigned(use cancel to cancel).
Response — 200 OK
{
"data": {
"updated": true,
"id": 21808,
"type": "desk",
"deskId": "room2-desk4",
"date": "2026-07-21",
"status": "assigned"
},
"error": null,
"meta": { "organization_id": "…" }
}
Desk edits cover single-day visitor bookings. A desk booking that's part of a
member plan (flex, monthly, ongoing…) or spans multiple days returns
member_booking_edit_unsupported; a whole-day / multi-day meeting-room group returns
group_edit_unsupported — cancel and re-create instead. An unknown deskId
returns desk_not_found, and editing an id that isn't yours returns
booking_not_found. Send an Idempotency-Key to make an edit safe to retry.
Errors
Errors use the same envelope with data: null and an error object.
Date-specific errors also carry the offending date.
{
"data": null,
"error": {
"code": "no_desks_available",
"message": "No desks are available on 2026-07-20. Please adjust the dates.",
"date": "2026-07-20"
}
} | Code | HTTP | Meaning |
|---|---|---|
missing_visitor_name | 400 | No visitorName was supplied. |
missing_visitor_email | 400 | A paid whole-day or office checkout needs a visitorEmail (email on office requests). |
company_required | 400 | An office request needs a companyName. |
no_dates | 400 | The dates array was empty. |
invalid_date | 400 | A date was not a valid YYYY-MM-DD. |
date_out_of_range | 400 | A date is in the past or beyond the space's booking window. |
invalid_return_url | 400 | returnUrl was set but is not an absolute http(s) URL. |
invalid_range | 400 | A from/to window is backwards, or a booking range ends before it starts. |
range_too_long | 400 | A bookings list window exceeds 366 days (or a whole-day range exceeds 60). |
invalid_id | 400 | A cancel/edit id is neither a meeting-room UUID nor a numeric desk-booking id. |
invalid_type | 400 | The bookings list type filter was not desk, room, or all. |
no_changes | 400 | A PATCH sent no editable fields. |
past_date | 400 | A whole-day or desk reschedule targeted a date before today. |
invalid_body | 400 | The request body was not valid JSON. |
org_not_found_or_disabled | 404 | Unknown space, or public booking is turned off. |
booking_not_found | 404 | No booking with that id belongs to your organization (also returned when editing/cancelling another org's booking). |
desk_not_found | 404 | A desk reschedule named a deskId that does not exist in your organization. |
no_desks_available | 409 | Every desk is taken on one of the requested dates. |
slot_taken | 409 | The meeting-room time slot, or the target desk + date, is already taken. |
booking_cancelled | 409 | A PATCH targeted an already-cancelled booking. |
group_edit_unsupported | 409 | A PATCH targeted a whole-day / multi-day group booking — cancel and re-create instead. |
member_booking_edit_unsupported | 409 | A PATCH targeted a desk booking that's part of a member plan (flex, monthly, ongoing…) or spans multiple days — not editable through the API yet. |
office_taken | 409 | The office already has an active agreement. |
processing | 409 | A booking with this Idempotency-Key is still being processed. |
unauthorized | 401 | No API key was sent on an authenticated endpoint. |
invalid_key | 401 | The API key is unknown or has been revoked. |
checkout_failed | 502 | Could not start Stripe checkout for a paid resource — safe to retry. |
payment_not_configured | 503 | Card payment is on for the resource, but the space hasn't finished its Stripe setup. |
What's next
Availability, desk, meeting-room, whole-day and office writes — including paid checkout for whole-day rooms and offices — plus listing, cancelling, and editing/rescheduling desk and meeting-room bookings with an API key are live today. Member-plan and multi-day desk edits, member and office-agreement endpoints, and more authenticated writes are on the way. Questions or early access? Email hello@ohmydesk.app.