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.
Install
Section titled “Install”npm i @mrcharles/sdkimport { createClient } from '@mrcharles/sdk';const mc = createClient();<script src="https://developers.mrcharles.app/sdk/v0/mrcharles.min.js"></script><script> const mc = MrCharles.createClient();</script>An ES module build is at /sdk/v0/mrcharles.esm.js for <script type="module">.
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.
One line: embed the booking screen
Section titled “One line: embed the booking screen”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.
Your own UI: the headless flow
Section titled “Your own UI: the headless flow”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 keysawait 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 gridflow.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 sawait flow.confirm({ paymentMode: 'on_site', notes });flow.state.confirmation.manageUrl; // show this to the gueststate.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 a503cannot double-book; - rotates the key and moves to
expiredwhen the hold dies (HOLD_EXPIRED,TOO_MANY_ATTEMPTS) or when the hold’sexpiresAtpasses on the client; - refuses a
paymentModethe room does not offer before touching the network; - fills
localefrom the site’s default when you don’t pass one.
Errors
Section titled “Errors”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}Languages
Section titled “Languages”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 Catalanmc.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.
Lessons with a teacher
Section titled “Lessons with a teacher”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 });In Astro
Section titled “In Astro”---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 launchconst 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.
Reference
Section titled “Reference”| Member | What |
|---|---|
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 / confirm | the 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 / slots | lesson reads |
links.site / bookRoom / manage / bookLesson / teacherProfile | every hand-off URL |
mountRoomBooking(target, opts) | iframe embed with resize and booked events |
roomBooking(opts) | the stateful flow |
startTimes, durationOptions, formatPrice, dateKey, addDays | helpers |
Source and changelog: packages/sdk in the monorepo. Versioned with the API: v0 tracks /api/v1.