Docs
Developers

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 URLYour workspace address, https://<your-workspace>.apros.app
AuthenticationNone. The endpoints are public.
CORSAny origin may call /api/public/booking/* from a browser, with GET, POST and OPTIONS.
FormatJSON. Send POST bodies with Content-Type: application/json.
TimesISO 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

MethodPathDoes
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}/rescheduleMoves a booking.
POST/api/public/booking/manage/{token}/cancelCancels a booking.

Build a booking flow

  1. 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"
      }
    }

    timezone is the organizer's time zone, in which their working hours are set. locale and timeFormat (12h or 24h) are your workspace's defaults; use them as starting values. Any of description, organizer.*, brand.* and locale can be null.

  2. Show free slots. GET /api/public/booking/{slug}/availability takes a window with from and to as 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.

  3. Create the booking. POST /api/public/booking/{slug} with the guest's details and one of the slots:

    FieldRequiredDescription
    nameYesThe guest's name, 1 to 200 characters.
    emailYesThe guest's email address, up to 255 characters.
    startAtYesOne of the slots from availability, exactly as returned.
    timezoneYesThe guest's IANA time zone, for example Europe/Warsaw. Used in emails and invitations.
    phoneNoUp to 50 characters. Saved on the booking and on the linked lead or contact if it has no phone yet.
    noteNoUp to 1000 characters. Shown in the confirmation and saved as a comment on the linked lead or contact.
    localeNoThe 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
      }
    }
    • joinUrl is the guest's link to the video call. They join from the browser without an account.
    • manageToken lets the guest manage the booking. Show them https://<your-workspace>.apros.app/book/manage/<manageToken>, or build reschedule and cancel into your widget with the endpoints below.
    • confirmationEmailSent is false when the guest's confirmation email could not be sent. The booking still exists, and the organizer is told in APROS.
  4. Handle a taken slot. Someone else may book the slot between your availability call and the guest's click. Then the API answers 409 with SLOT_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} returns bookingId, status, startAt, endAt, inviteeName, inviteeEmail, inviteeTimezone, joinUrl and link (slug, name, durationMin, timezone, organizer).
  • POST /api/public/booking/manage/{token}/reschedule moves the booking. Send { "newStartAt": "<slot>", "timezone": "<IANA zone>" }, where newStartAt is a free slot and timezone is optional. The response's data.booking is the updated booking.
  • POST /api/public/booking/manage/{token}/cancel cancels 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

CallsLimit
Create a booking30 per minute per IP address, and 10 per minute per booking link
Reschedule, cancel30 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

StatuscodeWhen
400VALIDATION_ERRORThe body or the query parameters are invalid; message says which.
400WINDOW_TOO_LARGEThe availability window is longer than 60 days.
403MANAGE_LINK_EXPIREDThe manage token has expired.
404LINK_NOT_FOUNDNo active booking link has this slug, or the Sales module is off.
404MODULE_DISABLEDThe Sales module is off for this workspace (when creating, checking availability or rescheduling).
404BOOKING_NOT_FOUNDNo booking has this manage token.
409SLOT_TAKENThe slot is no longer free.
409BOOKING_NOT_ACTIVEThe booking is no longer confirmed, for example because it was already cancelled.
429RATE_LIMITEDToo many requests; see Retry-After.
502CALL_CREATION_FAILEDThe video call could not be created, so the booking was cancelled. Try again.
500INTERNAL_ERRORAn unexpected error.