Skip to content

JavaScript SDK

@mrcharles/sdk wraps the public API so a website can take room bookings and show teacher lessons without writing fetch calls, state machines or postMessage handlers. Zero dependencies, about 11 kB gzipped, TypeScript types included.

Terminal window
npm i @mrcharles/sdk
import { createClient } from '@mrcharles/sdk';
const mc = createClient();

While building, use a studio in sandbox mode — demo-studio, or your own site with test mode on. Codes come back as hold.devCode, nothing is sent, and you switch to the real slug at launch; the client itself never changes.

mc.mountRoomBooking('#book', {
slug: 'studio-uno-madrid',
roomId: 'd9a36c09-d151-5d4a-a077-296293b07be4',
lang: 'es',
onBooked: ({ reference, manageUrl }) => { /* analytics, thank-you page… */ },
});

That drops our hosted booking screen into #book as an iframe, keeps its height in sync with the content, verifies the origin of every message, and calls onBooked once. Deposits, codes, legal text and the manage link all happen inside the frame. Pre-fill with date, start and durationMinutes; destroy() on the returned handle removes it.

When the design has to be yours, the SDK runs the flow and you render the state:

const flow = mc.roomBooking({ slug, roomId, onChange: (state) => render(state) });
await flow.loadSite(); // room, prices, timezone, captcha + Stripe keys
await flow.loadAvailability({ days: 14 }); // free spans in the studio's zone (never cached)
flow.startTimesFor('2026-10-20', 120); // ['09:00', '10:00', …] on the room's step grid
flow.durationsFor('2026-10-20', '10:00'); // [60, 120, 180] — only what fits
await flow.hold({ date, startTime, durationMinutes, guestName, channel: 'phone', contact: '+34600111222', captchaToken });
// state.step === 'verify'; flow.remainingMs() counts down to the server's expiresAt (5 minutes to verify)
await flow.verify(code); // flow.resend() after 60 s
await flow.confirm({ paymentMode: 'on_site', notes });
flow.state.confirmation.manageUrl; // show this to the guest

state.step is one of slot · details · verify · pay · done · expired. The flow:

  • generates one idempotency key per attempt and keeps it across network retries, so calling hold() again after a 503 cannot double-book;
  • rotates the key and moves to expired when the hold dies (HOLD_EXPIRED, TOO_MANY_ATTEMPTS) or when the hold’s expiresAt passes on the client;
  • refuses a paymentMode the room does not offer before touching the network;
  • fills locale from the site’s default when you don’t pass one.

Every failure is a MrCharlesError with the API’s stable code, the HTTP status, the X-Request-ID, and three helpers:

try { await flow.hold(details); }
catch (e) {
if (e.slotIsGone) { await flow.loadAvailability(); } // SLOT_TAKEN, OUTSIDE_HOURS, OUTSIDE_BOOKING_WINDOW
if (e.holdIsDead) { /* flow is already back at 'expired'; pick a time again */ }
if (e.isTransient) { /* network or 5xx — retry the same call */ }
showMessage(e.message); // safe to display as-is
}

One locale on the client drives everything: the hosted screen’s language, the guest’s SMS/email, Accept-Language, the SDK’s own strings and its formatting.

const mc = createClient({ locale: 'ca' }); // any BCP-47 tag: 'ca-ES', 'pt-BR'…
mc.mountRoomBooking('#book', { slug, roomId }); // screen in Catalan, code in Catalan
mc.t('action.confirm'); // "Confirmar la reserva"
mc.format.price(2500, 'EUR'); // "25,00 €"
mc.format.date('2026-10-20'); // "dimarts, 20 d’octubre"
mc.format.duration(90); // "1 h 30 min"
mc.pick(site.site.content.tagline, site.site.defaultLocale);
mc.setLocale('en'); // a language switcher: links, embeds, new flows follow
  • Hosted screen and messages speak es, ca, en (what the studio’s site offers).
  • The SDK speaks es, ca, en, fr, pt, it, de: labels and error sentences for a custom UI (flow.t(), flow.state.errorMessage, mc.errorMessage(err), messageFor()), formatting for any tag. A French site gets French labels; the guest’s code arrives in English, the closest hosted language.
  • No locale → each site’s default language, so a Spanish studio stays Spanish without configuration.

Students book lessons inside the Mr Charles app, where payment, reminders and cancellations live. The SDK gives you the public reads and the hand-off link:

const teacher = await mc.teachers.profile(teacherUserId);
const offers = await mc.teachers.offers(teacherUserId);
const slots = await mc.teachers.slots({ instructorUserId: teacher.user.id, offerId: offers[0].id, date: '2026-10-13', days: 7 });
// slots.days[].slots[] → { date, startTime, endTime } in slots.timezone
link.href = mc.links.bookLesson({ offerId: offers[0].id, instructorId: teacher.user.id });
---
import { createClient } from '@mrcharles/sdk';
const mc = createClient();
const site = await mc.sites.get(import.meta.env.PUBLIC_MC_STUDIO); // 'demo-studio' while building, the real slug at launch
const rooms = site.rooms.filter((r) => r.bookable);
---
<ul>{rooms.map((r) => <li><a href={mc.links.bookRoom({ slug: site.studio.slug, roomId: r.id, lang: 'es' })}>{r.name}</a></li>)}</ul>
<div id="book"></div>
<script>
import { createClient } from '@mrcharles/sdk';
createClient().mountRoomBooking('#book', { slug: import.meta.env.PUBLIC_MC_STUDIO, roomId: rooms0, lang: 'es' });
</script>

Reads (sites.get, teachers.*) are fine on the server. Availability and holds belong in the browser: rate limits are keyed by the connecting address and forwarding headers such as X-Forwarded-For are ignored, so if you proxy them every guest shares your server’s single bucket.

MemberWhat
createClient({ fetch, locale, headers })one client; setLocale, t, format, pick, errorMessage
sites.get(slug) · sites.resolve(host) · sites.availability(slug, roomId, { from, to })studio payload, custom-domain lookup, free spans
holds.create / verify / resend / confirmthe raw calls when you keep your own state
bookings.get(token) · bookings.cancel(token, reason)a guest’s booking by manage token
teachers.profile / offers / offer / windows / policy / slotslesson reads
links.site / bookRoom / manage / bookLesson / teacherProfileevery hand-off URL
mountRoomBooking(target, opts)iframe embed with resize and booked events
roomBooking(opts)the stateful flow
startTimes, durationOptions, formatPrice, dateKey, addDayshelpers

Source and changelog: packages/sdk in the monorepo. Versioned with the API: v0 tracks /api/v1.