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
- In the dashboard, open Bookings → Availability and set your weekly hours and time zone.
- Under Booking links, make one. Every link gets its own page, and its embed code.
- 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.
<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>// 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
| Field | Description |
|---|---|
linkrequiredstring | The link, as <workspace>/<link>: the end of its address. |
parentElement | string | Where 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. |
hideDetailsboolean | Hides the column with the host, length and price. |
brandingboolean | Shows “Booking by InlineMeet” even when your plan lets you leave it off. |
sessionstring | A booking session: books as that person. |
onEvent(name, data) => void | Every 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));| Event | When |
|---|---|
booking.viewed | The link opened. |
booking.slot_selected | They picked a time. |
booking.held | A paid booking’s time is held while they pay. |
booking.created | Booked: paid and confirmed, or confirmed for a free link. |
booking.rescheduled | They moved it. |
booking.cancelled | They cancelled it, with any refund. |
booking.resized | The 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-uiimport { 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)}
/>
);
}| Prop | Description |
|---|---|
linkrequiredstring | As <workspace>/<link>. |
originstring | Where InlineMeet is. Default https://inlinemeet.com. |
themeobject | Tokens without --im-: accent, surface, ink, soft, border, radius, font… |
colorScheme'light' | 'dark' | 'auto' | Default light. |
hideDetails, brandingboolean | As 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, object | A booking session, and who it is (shown in fixed fields). |
utmobject | Kept with the booking. |
manageTokenstring | Opens an existing booking — to move or cancel it — instead of the link’s times. |
onBooked(booking, manageToken) => void | Once it’s booked. |
onEvent(name, data) => void | The events above. |
backendBookingBackend | Talk 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
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 dayCalls 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.
- Make a page in your app for calls, with the meeting code in its address:
https://app.example.com/call/K7MQ-2PXD-9HRT. - 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}. - 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.joinUrlin the API and webhooks, and the host’s schedule, whose/meetlink 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).
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 });
}'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>
);
}REST API
Everything the widget does is in the API too, for your own screens. Authenticate with an API key.
The workspace’s links: id, slug, name, length, price, its page’s address.
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", "…"]
}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"
}'?when=upcoming|past|all, ?status=confirmed|cancelled|held, ?link=<id>, ?code=<meeting code>.
{ "start": ISO }, a time the link offers. Any payment carries over.
{ "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.
{ "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.
{
"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.
Plans and limits
| Free | Paid plans | |
|---|---|---|
| Booking links | Unlimited, for everyone in the workspace | Unlimited, for everyone in the workspace |
| Taking payments | Yes, 2% fee | Yes, 2% fee |
| Booked calls at once | One | One per host licence |
| A paid booked call | Up to 2 hours, no wait between calls | Up to the plan’s meeting length |
| A free booked call | Up to 30 minutes, at least two hours apart | Up to the plan’s meeting length |
| REST API, sessions and webhooks | No | Yes |
| “Booking by inlinemeet” | Shown | Shown on Pro; hidden on Business and Enterprise |