Booking API
Build your own booking widget on top of an APROS booking link — load the link, show free slots, create a booking, and reschedule or cancel it. Public endpoints, no API key.
APROS booking pages let visitors book a meeting with you from a public link. The Booking API is the same data as a set of public endpoints, so you can build your own booking widget on any website: read a booking link, show its free slots, create a booking, and let the guest reschedule or cancel.
Each booking creates a scheduled video call and a calendar event in APROS, and by default links the guest to a lead in your CRM.
Before you start
- You need a booking link. In APROS, open Calendar, select Booking and create one on the My Booking Links tab. The link must be Active.
- Your workspace needs the Sales module on. If it is off, the endpoints answer
404. - Note the link's slug: the last part of its public URL,
/book/<slug>. Select Copy link on the link to get the full URL.
How it works
| Base URL | Your workspace address, https://<your-workspace>.apros.app |
| Authentication | None. The endpoints are public. |
| CORS | Any origin may call /api/public/booking/* from a browser, with GET, POST and OPTIONS. |
| Format | JSON. Send POST bodies with Content-Type: application/json. |
| Times | ISO 8601 in UTC with a Z, for example 2026-11-03T10:00:00.000Z (what JavaScript's toISOString() produces). Times with another offset are refused. Convert to the visitor's time zone only for display. |
A different response shape
The Booking API wraps every response, unlike the main API. Success is
{ "success": true, "data": { ... } } and failure is
{ "success": false, "error": { "code": "...", "message": "..." } }.
Endpoints
| Method | Path | Does |
|---|---|---|
GET | /api/public/booking/{slug} | Returns the link's details. |
GET | /api/public/booking/{slug}/availability?from=...&to=... | Returns free slots. |
POST | /api/public/booking/{slug} | Creates a booking. |
GET | /api/public/booking/manage/{token} | Returns a booking. |
POST | /api/public/booking/manage/{token}/reschedule | Moves a booking. |
POST | /api/public/booking/manage/{token}/cancel | Cancels a booking. |
Build a booking flow
-
Load the link.
GET /api/public/booking/{slug}returns what you need to draw the widget:{ "success": true, "data": { "name": "Intro call", "description": "A 30-minute first conversation", "durationMin": 30, "timezone": "Europe/Kyiv", "showAvatar": true, "organizer": { "name": "Olena Koval", "avatar": "https://..." }, "brand": { "companyName": "Koval Studio", "logo": "https://...", "primaryColor": "#0f766e" }, "locale": "en", "timeFormat": "24h" } }timezoneis the organizer's time zone, in which their working hours are set.localeandtimeFormat(12hor24h) are your workspace's defaults; use them as starting values. Any ofdescription,organizer.*,brand.*andlocalecan benull. -
Show free slots.
GET /api/public/booking/{slug}/availabilitytakes a window withfromandtoas ISO date-times, at most 60 days long, and returns the start time of every free slot:{ "success": true, "data": { "slots": ["2026-11-03T10:00:00.000Z", "2026-11-03T10:30:00.000Z"] } }Each meeting lasts
durationMin. The slots already respect the organizer's weekly availability, their existing calendar events and bookings, the link's buffer between meetings, and a minimum notice of 60 minutes from now. Do not cache slots: they change with every booking. -
Create the booking.
POST /api/public/booking/{slug}with the guest's details and one of the slots:Field Required Description nameYes The guest's name, 1 to 200 characters. emailYes The guest's email address, up to 255 characters. startAtYes One of the slots from availability, exactly as returned.timezoneYes The guest's IANA time zone, for example Europe/Warsaw. Used in emails and invitations.phoneNo Up to 50 characters. Saved on the booking and on the linked lead or contact if it has no phone yet. noteNo Up to 1000 characters. Shown in the confirmation and saved as a comment on the linked lead or contact. localeNo The language your widget was shown in, for example uk. Used for the emails.A created booking answers
201:{ "success": true, "data": { "bookingId": "3d0b7e52-9a4f-4c1e-8b6d-2f5a7c9e1b3d", "manageToken": "...", "joinUrl": "https://...", "startAt": "2026-11-03T10:00:00.000Z", "endAt": "2026-11-03T10:30:00.000Z", "confirmationEmailSent": true } }joinUrlis the guest's link to the video call. They join from the browser without an account.manageTokenlets the guest manage the booking. Show themhttps://<your-workspace>.apros.app/book/manage/<manageToken>, or build reschedule and cancel into your widget with the endpoints below.confirmationEmailSentisfalsewhen the guest's confirmation email could not be sent. The booking still exists, and the organizer is told in APROS.
-
Handle a taken slot. Someone else may book the slot between your
availabilitycall and the guest's click. Then the API answers409withSLOT_TAKEN: reload the slots and ask the guest to pick again.
Manage a booking
All three calls take the manageToken from the created booking.
GET /api/public/booking/manage/{token}returnsbookingId,status,startAt,endAt,inviteeName,inviteeEmail,inviteeTimezone,joinUrlandlink(slug,name,durationMin,timezone,organizer).POST /api/public/booking/manage/{token}/reschedulemoves the booking. Send{ "newStartAt": "<slot>", "timezone": "<IANA zone>" }, wherenewStartAtis a free slot andtimezoneis optional. The response'sdata.bookingis the updated booking.POST /api/public/booking/manage/{token}/cancelcancels it. The response is{ "success": true, "data": { "bookingId": "..." } }.
A manage token stops working 24 hours after the meeting's end; after that the calls answer 403
with MANAGE_LINK_EXPIRED.
Example widget
A minimal widget in plain JavaScript that lists this month's slots and books the one the visitor clicks:
<div id="booking"></div>
<script type="module">
const BASE = 'https://<your-workspace>.apros.app';
const SLUG = 'intro-call';
const visitorTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
async function api(path, options = {}) {
const res = await fetch(BASE + path, {
...options,
headers: { 'Content-Type': 'application/json', ...options.headers },
});
const body = await res.json();
if (!body.success) {
throw Object.assign(new Error(body.error.message), { code: body.error.code });
}
return body.data;
}
async function render() {
const link = await api(`/api/public/booking/${SLUG}`);
const from = new Date();
const to = new Date(from.getTime() + 30 * 24 * 60 * 60 * 1000);
const { slots } = await api(
`/api/public/booking/${SLUG}/availability?from=${from.toISOString()}&to=${to.toISOString()}`,
);
const format = new Intl.DateTimeFormat(link.locale ?? undefined, {
timeZone: visitorTz,
dateStyle: 'medium',
timeStyle: 'short',
hour12: link.timeFormat === '12h',
});
const root = document.getElementById('booking');
root.replaceChildren();
for (const slot of slots) {
const button = document.createElement('button');
button.textContent = format.format(new Date(slot));
button.onclick = () => book(slot);
root.append(button);
}
}
async function book(startAt) {
try {
const booking = await api(`/api/public/booking/${SLUG}`, {
method: 'POST',
body: JSON.stringify({
name: 'Ivan Petrenko',
email: 'ivan@example.com',
startAt,
timezone: visitorTz,
}),
});
alert(`Booked. Join the call at ${booking.joinUrl}`);
} catch (error) {
if (error.code === 'SLOT_TAKEN') {
alert('That time was just taken. Please pick another.');
render();
} else {
alert(error.message);
}
}
}
render();
</script>In a real widget, collect the guest's name and email in a form before you call book.
Rate limits
| Calls | Limit |
|---|---|
| Create a booking | 30 per minute per IP address, and 10 per minute per booking link |
| Reschedule, cancel | 30 per minute per IP address, shared with creating bookings |
Over a limit, the API answers 429 with RATE_LIMITED and a Retry-After header in seconds.
Errors
| Status | code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | The body or the query parameters are invalid; message says which. |
| 400 | WINDOW_TOO_LARGE | The availability window is longer than 60 days. |
| 403 | MANAGE_LINK_EXPIRED | The manage token has expired. |
| 404 | LINK_NOT_FOUND | No active booking link has this slug, or the Sales module is off. |
| 404 | MODULE_DISABLED | The Sales module is off for this workspace (when creating, checking availability or rescheduling). |
| 404 | BOOKING_NOT_FOUND | No booking has this manage token. |
| 409 | SLOT_TAKEN | The slot is no longer free. |
| 409 | BOOKING_NOT_ACTIVE | The booking is no longer confirmed, for example because it was already cancelled. |
| 429 | RATE_LIMITED | Too many requests; see Retry-After. |
| 502 | CALL_CREATION_FAILED | The video call could not be created, so the booking was cancelled. Try again. |
| 500 | INTERNAL_ERROR | An unexpected error. |