@mrcharles/sdk
v0.2.0
Published
Mr Charles booking SDK — put studio room bookings and teacher lessons on any website.
Maintainers
Readme
@mrcharles/sdk
Put Mr Charles booking on any website: studio rooms (guests, no account) and
teacher lessons (hand-off to the app). Zero dependencies, ~6 kB min+gz, works
in the browser, in Astro/Next/SvelteKit server code, and as a plain <script>.
Docs: https://developers.mrcharles.app/integrate/sdk/
Install
npm i @mrcharles/sdkor without a bundler:
<script src="https://developers.mrcharles.app/sdk/v0/mrcharles.min.js"></script>
<!-- exposes window.MrCharles -->30-second version: embed our booking screen
import { createClient } from '@mrcharles/sdk';
const mc = createClient();
mc.mountRoomBooking('#book', {
slug: 'studio-uno-madrid',
roomId: 'd9a36c09-d151-5d4a-a077-296293b07be4',
lang: 'es',
onBooked: ({ reference, manageUrl }) => console.log('booked', reference, manageUrl),
});The iframe resizes to its content, takes card deposits, sends the SMS code, and shows the manage link. You write nothing else.
Your own UI: the headless flow
const flow = mc.roomBooking({ slug, roomId, onChange: render });
await flow.loadSite(); // rooms, prices, timezone, captcha/stripe keys
await flow.loadAvailability({ days: 14 }); // free spans, studio's zone
flow.startTimesFor('2026-10-20', 120); // → ['09:00', '10:00', …]
flow.durationsFor('2026-10-20', '10:00'); // → [60, 120, 180]
await flow.hold({ date, startTime, durationMinutes, guestName, channel: 'phone', contact: '+34600111222' });
// state.step === 'verify' · flow.remainingMs() counts down from 12 min
await flow.verify(code); // 6 digits; flow.resend() after 60 s
await flow.confirm({ paymentMode: 'on_site', notes }); // state.step === 'done'
flow.state.confirmation.manageUrl; // hand this to the guestEvery error is a MrCharlesError with a stable code (SLOT_TAKEN,
HOLD_EXPIRED, CODE_INVALID, …) and three helpers: slotIsGone,
holdIsDead, isTransient. The flow rotates the idempotency key when a hold
dies and keeps it across network retries, so you can simply call hold() again.
Lessons with a teacher
const t = await mc.teachers.profile(teacherUserId);
const offers = await mc.teachers.offers(teacherUserId);
const slots = await mc.teachers.slots({ instructorUserId: t.user.id, offerId: offers[0].id, date: '2026-10-13', days: 7 });
// <a href={mc.links.bookLesson({ offerId: offers[0].id, instructorId: t.user.id })}>Book</a>Students book inside the Mr Charles app (payment, reminders, cancellations), so the SDK gives you the reads and the link, not a guest checkout.
Astro
---
import { createClient } from '@mrcharles/sdk';
const mc = createClient();
const site = await mc.sites.get('studio-uno-madrid'); // at build / on request
---
<h1>{site.studio.name}</h1>
{site.rooms.filter((r) => r.bookable).map((r) => <a href={mc.links.bookRoom({ slug: site.studio.slug, roomId: r.id })}>{r.name}</a>)}
<div id="book"></div>
<script>
import { createClient } from '@mrcharles/sdk';
createClient().mountRoomBooking('#book', { slug: 'studio-uno-madrid', roomId: '…' });
</script>Availability and holds must run in the browser (or forward the guest's IP in
headers: { 'X-Forwarded-For': ip } when you proxy them).
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 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.
API surface
| | |
|---|---|
| createClient({ fetch, locale, headers }) | one client; setLocale, t, format, pick, errorMessage |
| sites.get / resolve / availability | studio payload, custom-domain lookup, free spans |
| holds.create / verify / resend / confirm | the raw calls, if you manage state yourself |
| bookings.get / cancel | a guest's booking by manage token |
| teachers.profile / offers / offer / windows / policy / slots | lesson reads |
| links.site / bookRoom / manage / bookLesson / teacherProfile | URLs for every hand-off |
| mountRoomBooking(target, opts) | iframe embed with resize + booked events |
| roomBooking(opts) | the stateful flow above |
| startTimes / durationOptions / formatPrice / dateKey / addDays | helpers |
Testing without bothering anyone
There is one API. To develop, point the SDK at a studio site in sandbox
mode — the platform's demo-studio, or your own site with Modo pruebas
switched on in the dashboard. A sandbox site returns the verification code
as hold.devCode, sends no SMS or email, offers on_site only, and purges
its bookings after seven days. site.site.sandbox tells you which mode you
are in; switch the slug to the real one at launch and nothing else changes.
