Skip to content
Bookings

Guides

Bookings

Booking links people outside book calls with you from: on your booking page, embedded in your website with a script or a React component, or from your own screens through the API. Each booking becomes an InlineMeet meeting, and can be paid for at booking.

How bookings work

A booking link is one kind of call people can book with someone in your workspace: its length, a price or none, the questions asked, and how much notice is needed. It lives at inlinemeet.com/book/<workspace>/<link>, and your workspace’s booking page lists the links you choose to show.

The times a link offers come from its host’s hours under Availability, or from hours of its own, less their InlineMeet meetings, other bookings, and the busy times in their calendar address if they’ve added one. A link with its own hours still keeps to the host’s days that differ: an away day takes no bookings, and a day with other hours offers only where the two overlap. Times are offered only while the workspace has a host licence free, so a booked call can always start.

A booking becomes a scheduled meeting with its own meeting code and join link, set up from the link’s template. A free link’s booking is confirmed at once. A paid one is held for 10 minutes while the invitee pays, and confirmed when the payment goes through.

The template also decides the logo. A link’s page shows your workspace logo at the top of its details when the link has a template whose logo is switched on, and the link’s workspace.logoUrl carries it to embeds and the React component. A link without a template shows no logo, only the host’s initials.

Set up

  1. In the dashboard, open Bookings → Availability and set your weekly hours and time zone.
  2. Under Booking links, make one. Every link gets its own page, and its embed code.
  3. To charge for bookings, open Payments and connect your Stripe account (the workspace’s owner or an account manager does this once).

Embed with a script

One script tag works on any website. The widget runs in an iframe from InlineMeet, so card details go straight to Stripe and never touch your page. It sizes itself to fit.

Inline
<div id="book"></div>
<script src="https://inlinemeet.com/embed/booking.js"></script>
<script>
  InlineMeet.booking.inline({
    link: 'malik-advisory/consultation',
    parent: document.getElementById('book'),
  });
</script>
Pop-ups
// A floating button in the corner
InlineMeet.booking.popupButton({ link: 'malik-advisory/consultation', text: 'Book a call' });

// Or open it from any element you already have
InlineMeet.booking.popupOn(document.querySelector('#book-now'), { link: 'malik-advisory/intro-call' });

// Or with no script of your own
<div data-inlinemeet-booking="malik-advisory/consultation"></div>
<a href="#" data-inlinemeet-booking-popup="malik-advisory/intro-call">Book an intro call</a>

Options

FieldDescription
linkrequiredstringThe link, as <workspace>/<link>: the end of its address.
parentElement | stringWhere an inline widget goes (inline only).
prefillobject{ name, email, answers: { [questionId]: text } }, filled in for them.
utmobject{ source, medium, campaign, term, content }, kept with the booking.
themeobject{ accent, text, background } as hex colours.
scheme'light' | 'dark' | 'auto'Light (the default), dark, or the visitor’s own setting.
hideDetailsbooleanHides the column with the host, length and price.
brandingbooleanShows “Booking by InlineMeet” even when your plan lets you leave it off.
sessionstringA booking session: books as that person.
onEvent(name, data) => voidEvery event from this widget.

Events

InlineMeet.booking.on('booking.created', (booking) => {
  // { bookingId, start, end, invitee: { name, email }, payment, joinUrl }
  analytics.track('Booked a call', { value: booking.payment ? booking.payment.amount / 100 : 0 });
});

// Every event, with its name
InlineMeet.booking.on('*', (data, name) => console.log(name, data));
EventWhen
booking.viewedThe link opened.
booking.slot_selectedThey picked a time.
booking.heldA paid booking’s time is held while they pay.
booking.createdBooked: paid and confirmed, or confirmed for a free link.
booking.rescheduledThey moved it.
booking.cancelledThey cancelled it, with any refund.
booking.resizedThe widget’s height changed (the script uses it to fit the iframe).

The React component

@inlinemeet/booking-ui draws the widget in your own page instead of an iframe, and takes the same --im-* theme tokens as the call UI: theme your calls, and your bookings match. Stripe’s payment form still runs in Stripe’s own frames.

npm install @inlinemeet/booking-ui
BookConsultation.tsx
import { BookingWidget } from '@inlinemeet/booking-ui';
import '@inlinemeet/booking-ui/styles.css';

export function BookConsultation({ user, session }) {
  return (
    <BookingWidget
      link="malik-advisory/consultation"
      session={session}
      sessionInvitee={{ name: user.name, email: user.email }}
      theme={{ accent: '#0f766e', surface: '#ffffff', ink: '#12312d' }}
      onBooked={(booking, manageToken) => router.push('/my-calls')}
      onEvent={(name, data) => analytics.track(name, data)}
    />
  );
}
PropDescription
linkrequiredstringAs <workspace>/<link>.
originstringWhere InlineMeet is. Default https://inlinemeet.com.
themeobjectTokens without --im-: accent, surface, ink, soft, border, radius, font…
colorScheme'light' | 'dark' | 'auto'Default light.
hideDetails, brandingbooleanAs in the script: no details column; “Booking by InlineMeet” whatever the plan.
layout'card' | 'page'A card in your page (the default), or page: the whole page, edge to edge and at least as tall as the window, like InlineMeet’s own booking pages.
prefillobject{ name, email, answers }
session, sessionInviteestring, objectA booking session, and who it is (shown in fixed fields).
utmobjectKept with the booking.
manageTokenstringOpens an existing booking — to move or cancel it — instead of the link’s times.
onBooked(booking, manageToken) => voidOnce it’s booked.
onEvent(name, data) => voidThe events above.
backendBookingBackendTalk to something else: createDemoBackend() answers from memory, for demos and tests.

Booking sessions

When the person booking is signed in to your app, your server can tell InlineMeet who they are. The widget then books as them — name and email filled in and fixed — and the booking carries your own id for them, so you can match it to your user.

Your server
// Your server
const res = await fetch('https://api.inlinemeet.com/v1/booking-sessions', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.INLINEMEET_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ invitee: { id: user.id, name: user.name, email: user.email } }),
});
const { session } = await res.json(); // lasts a day

Calls in your own app

A booked call happens on InlineMeet unless you say otherwise: its links go to inlinemeet.com/meet/<code>. If the people you meet already use your own website or app, the call can happen there instead, drawn by the call UI. It needs a paid plan, for the API.

  1. Make a page in your app for calls, with the meeting code in its address: https://app.example.com/call/K7MQ-2PXD-9HRT.
  2. In the dashboard, open the booking link and choose In your own app under Where it happens. Give your page’s address with {code} where the code goes: https://app.example.com/call/{code}.
  3. From then on, every way into a booking’s call opens your page: the confirmation and reminder emails, the calendar invite, the invitee’s manage page, meeting.joinUrl in the API and webhooks, and the host’s schedule, whose /meet link sends them on to your page.

Your page reads the code, and your server lets each person in with an API key, as in the quickstart: it opens the call with POST /v1/rooms and gets them a join token with POST /v1/rooms/:code/tokens. GET /v1/bookings?code=… says whose booking it is, so you can decide who gets in and make the host a moderator. invitee.id is your own id for the person who booked, when your app booked as them (a booking session or POST /v1/bookings).

app/api/call/[code]/token/route.ts
const API = 'https://api.inlinemeet.com/v1';
const headers = {
  Authorization: `Bearer ${process.env.INLINEMEET_API_KEY}`,
  'Content-Type': 'application/json',
};

export async function POST(_req: Request, { params }: { params: Promise<{ code: string }> }) {
  const { code } = await params;
  const user = await currentUser(); // your own sign-in

  // Whose booking is this? Let in its host and the person who booked, nobody else.
  const query = new URLSearchParams({ code, when: 'all', status: 'confirmed' });
  const { bookings } = await fetch(`${API}/bookings?${query}`, { headers }).then((r) => r.json());
  const booking = bookings?.[0];
  const isHost = user.isStaff; // your own rule: say, the person whose link it is
  const isInvitee = booking?.invitee.id === user.id || booking?.invitee.email === user.email;
  if (!booking || (!isHost && !isInvitee)) {
    return Response.json({ error: { code: 'forbidden', message: 'This call isn’t yours.' } }, { status: 403 });
  }

  // Open the call: set up from the link's template, on the host licence kept for it.
  await fetch(`${API}/rooms`, { method: 'POST', headers, body: JSON.stringify({ code }) });

  const res = await fetch(`${API}/rooms/${encodeURIComponent(code)}/tokens`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      identity: `${user.id}:${crypto.randomUUID()}`, // unique per tab
      name: user.name,
      role: isHost ? 'moderator' : 'participant',
      publish: { microphone: true, camera: true, screenShare: true },
    }),
  });
  return Response.json({ ...(await res.json()), name: user.name }, { status: res.status });
}
app/call/[code]/page.tsx
'use client';
import { useMemo } from 'react';
import { useParams } from 'next/navigation';
import { CallRoom, createFetchBackend } from '@inlinemeet/call-ui';

export default function CallPage() {
  const { code } = useParams<{ code: string }>();
  // Points the call UI at your token route. Moderation and recording need routes of their own.
  const backend = useMemo(
    () => createFetchBackend({ basePath: `/api/call/${code}`, moderation: false, recordings: false }),
    [code],
  );

  return (
    <main style={{ height: '100dvh' }}>
      <CallRoom backend={backend} title="Your call" />
    </main>
  );
}
The room your server opens with a booking’s code is that booking’s call: it’s set up from the link’s template (unless you name another), and it takes the host licence kept for that booking, so it always starts on time.

REST API

Everything the widget does is in the API too, for your own screens. Authenticate with an API key.

GET/v1/booking-links

The workspace’s links: id, slug, name, length, price, its page’s address.

GET/v1/booking-links/:slug/slots

The times a link offers in ?from=…&to=… (ISO; the next two weeks by default, 62 days at most).

curl -G https://api.inlinemeet.com/v1/booking-links/consultation/slots \
  -H "Authorization: Bearer $INLINEMEET_KEY" \
  -d from=2026-11-02T00:00:00Z -d to=2026-11-09T00:00:00Z

{
  "from": "2026-11-02T00:00:00.000Z",
  "to": "2026-11-09T00:00:00.000Z",
  "slots": ["2026-11-02T09:00:00.000Z", "2026-11-02T10:00:00.000Z", "…"]
}
POST/v1/bookings

Books a time. A free link’s booking comes back confirmed, with its meeting. A paid one comes back held, with payment to confirm in Stripe’s Payment Element: it’s confirmed — and booking.created sent — once the payment goes through.

curl https://api.inlinemeet.com/v1/bookings \
  -H "Authorization: Bearer $INLINEMEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "link": "consultation",
    "start": "2026-11-02T10:00:00Z",
    "invitee": { "id": "user_42", "name": "Hamza Ali", "email": "hamza@example.com" },
    "answers": { "q8f2a1c": "Pricing for our second product" },
    "timezone": "Asia/Karachi"
  }'
GET/v1/bookings

?when=upcoming|past|all, ?status=confirmed|cancelled|held, ?link=<id>, ?code=<meeting code>.

GET/v1/bookings/:id
POST/v1/bookings/:id/reschedule

{ "start": ISO }, a time the link offers. Any payment carries over.

POST/v1/bookings/:id/cancel

{ "reason"?, "refund"?: "full" | "policy" | "none" | amount, "by"?: "host" | "invitee" }. As the host (the default) it refunds in full; on the invitee’s behalf it follows the link’s cancellation policy.

POST/v1/booking-sessions

{ "invitee": { "id", "name", "email" } } → { session, expiresAt }. See booking sessions.

Webhooks

Bookings send booking.created, booking.rescheduled (with from), booking.cancelled (with any refund), payment.succeeded and payment.refunded to your webhook endpoint, signed and retried like every other event. Each carries the booking as it was when it happened.

booking.created
{
  "id": "evt_3c1d9e…",
  "type": "booking.created",
  "createdAt": "2026-11-01T16:33:12.000Z",
  "data": {
    "booking": {
      "id": "6ad73722b7b62d188d2fbc1c",
      "link": { "id": "6ad7…", "slug": "consultation", "name": "Consultation" },
      "hostUserId": "6ad739b1…",
      "hostName": "Sara Malik",
      "start": "2026-11-02T10:00:00.000Z",
      "end": "2026-11-02T10:45:00.000Z",
      "timezone": "Europe/London",
      "status": "confirmed",
      "invitee": { "name": "Hamza Ali", "email": "hamza@example.com", "id": "user_42" },
      "answers": [{ "label": "What would you like help with?", "value": "Pricing for our second product" }],
      "payment": { "amount": 10000, "currency": "usd", "status": "paid", "refunded": 0, "fee": 200, "feeRefunded": 0 },
      "meeting": { "code": "K7MQ-2PXD-9HRT", "joinUrl": "https://inlinemeet.com/meet/K7MQ-2PXD-9HRT" },
      "cancelled": null,
      "rescheduledFrom": null,
      "utm": { "source": "website" },
      "createdAt": "2026-11-01T16:32:10.000Z"
    }
  }
}

Payments and refunds

  • Payments go into your workspace’s own Stripe account, connected from Bookings → Payments. InlineMeet never holds the money.
  • InlineMeet keeps 2% of each payment, as an application fee on the same charge. Stripe’s own card fee comes out of your Stripe account as usual.
  • Refunds give back the same share of InlineMeet’s fee. An invitee cancelling before the link’s refund window closes is refunded in full automatically; after it, nothing is refunded unless you refund by hand. You cancelling always refunds in full.
  • Moving a booking carries its payment over: nothing new is charged.
  • If a payment hasn’t gone through within 10 minutes, the time goes back on offer and the payment is cancelled.
Test everything with Stripe’s test mode first: connect a test account and pay with the card 4242 4242 4242 4242.

Plans and limits

FreePaid plans
Booking linksUnlimited, for everyone in the workspaceUnlimited, for everyone in the workspace
Taking paymentsYes, 2% feeYes, 2% fee
Booked calls at onceOneOne per host licence
A paid booked callUp to 2 hours, no wait between callsUp to the plan’s meeting length
A free booked callUp to 30 minutes, at least two hours apartUp to the plan’s meeting length
REST API, sessions and webhooksNoYes
“Booking by inlinemeet”ShownShown on Pro; hidden on Business and Enterprise