@blazeo.com/calendar-client
Private package – for Blazeo internal teams only. JavaScript client for the Blazeo Calendar / Appointment API. Use it in React (or any JS app) to call calendar and event endpoints via MobX State Tree models.
Install
From your npm registry (internal teams; ensure you're logged in to the scope):
npm install @blazeo.com/calendar-client
Or link locally:
cd calendar-client && npm run build && npm link
cd your-app && npm link @blazeo.com/calendar-client
Usage
Configure once at app startup, then use models and their methods:
import {
configure,
setAccessToken,
setGetAccessToken,
CalendarModel,
createRootStore,
} from '@blazeo.com/calendar-client';
configure({
baseUrl: 'https://your-appointment-api.example.com',
consumer: 'my-app', // optional Consumer header
accessToken: yourIdpAccessToken, // JWT from your identity provider
expiresAtUtc: '2026-05-21T12:00:00Z', // optional — used before auto-refresh
getAccessToken: async () => yourIdp.getAccessToken(), // optional refresh callback
});
// Calendar static methods (no store needed)
const timezones = await CalendarModel.getTimeZones();
const calendar = await CalendarModel.get('calendar-guid');
// Or use RootStore with models
const store = createRootStore();
const cal = store.addCalendar({ calendarId: 'my-cal', name: 'My Calendar' });
await cal.create(); // POST to backend
JWT authentication
The Appointment API validates Bearer JWTs from your identity provider (Authentication:Jwt:Authority on the server). This package does not exchange api keys for tokens — pass the JWT your app already has.
| Function | Purpose |
|---|---|
setAccessToken(token, expiresAtUtc?) |
Store JWT from your IdP |
setGetAccessToken(fn) |
Async callback to refresh when missing/near expiry |
ensureValidAccessToken() |
Resolve token before a request (uses callback if needed) |
clearAuth() |
Clear token and refresh callback |
All reqGet / reqPost calls attach Authorization: Bearer … when a token is configured.
setAccessToken(session.accessToken, session.expiresAt);
setGetAccessToken(() => authService.acquireTokenSilent());
await LeadModel.requestExport('company-key', { userEmail: 'user@example.com' });
API overview
- CalendarModel (static):
get,getByCompany,getTimeZones,getTimeZone,getParticipants,getMonth,getEvents, etc. - EventModel (instance):
get,create,cancel,delete,getCancellable,getAvailability,setReminder - EventModel (static):
get,cancel,delete,getCancellable,createEvent,reschedule,updateEvent, …GET /event/cancelrequires a bearer token (setAccessToken), likedelete. The token, or theX-User-Email/X-User-Display-Name/X-User-Idheaders a service token forwards, is what the AppointmentCanceled activity entry records ashandledBy.cancel/deleteresolve to the API envelope whosedatais anEventCancellationData: besidesEventIdandStatusit carries the appointment'sSource/LeadSource,Host(participant),Location,CustomLocation,CalendarLocationValue,LeadId, dates and visitor details, so a cancellation can be recorded against the lead without re-fetching the event. The same object is the payload of theevent/cancel/event/deleteConsumer webhooks. - ActivityLogModel:
get(GET /activitylog/get). Each entry exposessource/leadSource,location/customLocation/calendarLocationValue,handledBy(who performed the action) andhost(the appointment's participant) for scheduled, rescheduled and canceled appointment activities. Canceled entries also carrycancellationReason, and theirsummaryis that reason when one was given (booking notes otherwise). - FlowModel: Same pattern as Calendar —
FlowModel.create({}, { env })with no fields; staticget,getRaw,list,createFlow,updateFlow,delete,duplicate, appearance/embed/public/preview helpers; instance methods mirror those usingflowIdon the snapshot - LeadModel:
LeadModel.create({}, { env }); staticget,getRaw,getByEmail,getByPhone,getByCompany,searchByCompany,saveColumnSelection,getColumnSelection,allowedColumns,requestExport; instanceget,getByEmail,getByPhone,getByCompany,searchByCompany,saveColumnSelection,getColumnSelection,requestLeadExport. Pass{ all: true }togetByEmailto include soft-deleted leads (GET /lead/getbyemail?all). PassuserIdingetByCompany(companyKey, { userId })to return only columns saved for that user.filtersonfirst_name,last_name, andemailthat share the same text match when any of those fields contains it; other filters are combined together.searchByCompanycallsGET /lead/company/searchwith the same arguments and returns a lead when any filter matches. ForrequestExport, passuserEmail(or rely on JWT email claim) so the server can publish aNotificationGeneratedmessage when export completes. - ParticipantModel: static
get,getByEmail,getByIds,getAll, …; instancegetByEmailusesemail+companyKeyon the snapshot (seeGET /participant/getbyemail) - AuthModel (calendar OAuth / Connect Calendar):
getCalendarProviders,getAuthorizationUrl,startDirectAuthorization,getAuthorizationStatus,getDirectAuthorizationStatus,openOAuthPopup,onCalendarAuthMessage,waitForAuthorization,waitForDirectAuthorization— see Calendar authorization flow - RootStore:
addCalendar,addEvent
Booking for a lead that already exists
When the booking is made from a lead the caller already has open (CRM lead page, SmartHub), send that
lead's leadId. The booking is then attributed to that lead instead of being matched on the visitor
details, so editing the email or phone on the form cannot create a second lead or raise a "new lead"
notification:
await EventModel.createEvent({
calendarId,
leadId,
visitorName: 'Jane Dyk',
visitorEmail: 'jane@example.com',
startDate, endDate, startHour, startMinute, endHour, endMinute,
}, { source: 'SmartHub' });
A leadId from another company is ignored, and the booking falls back to matching on email, then on
the phone of a lead that has no email of its own. Omit leadId for public bookings, where the visitor
is not a known lead yet.
Custom field order
Each field defined on a calendar has a sortOrder. CustomFieldModel.getAll and
CustomFieldModel.getForm return fields with the lowest sortOrder first, then by created time.
Send sortOrder on add or on each item in saveForm. When saveForm omits it, the array
position is stored, so the list you submit is the order visitors see.
CRM lead custom fields on a booking
Answers to CRM lead custom fields travel in crmLeadCustomFieldsData, keyed by field label. Those
option lists live in the CRM, not in the calendar, so an option id cannot be translated back to text
here — send both halves or the confirmation email and booking details show the raw id:
await EventModel.createEvent({
calendarId,
visitorEmail: 'lead@example.com',
startDate, endDate, startHour, startMinute, endHour, endMinute,
crmLeadCustomFieldsData: {
'Add Country': { id: '2', value: 'Pakistan' },
'Add Language': { id: '192', value: 'Urdu' },
'Add Interests': [{ id: '5', value: 'Sports' }, { id: '7', value: 'Music' }],
'How did you hear about us?': 'A friend',
},
}, { source: 'Booking Page' });
id is what the CRM receives; value is what people see. Free-text answers stay plain strings, and
a bare id still works for backward compatibility (it just renders as the id). On the instance API the
same data is passed through the create options: event.create({ offset, crmLeadCustomFieldsData }).
Per-calendar webhooks on booking
Both EventModel.createEvent and event.create() repeat the calendar id on the request URL as
POST /event/create?calendar_id=<calendarId>. The endpoint itself ignores it; it is there so a
Consumer webhook (Appointment.ConsumerEvent) can be registered for a single calendar rather than
every booking the consumer makes:
event/create -> every booking by this consumer
event/create?calendar_id=2e789bbf-8eb9-42f0-bc8e-6044542f15d7 -> bookings on that calendar only
The registered path must be lower-case; the parameter is omitted when the payload has no calendar id.
Calendar authorization (direct UI)
When the user connects Google or Outlook from the Scheduling modal:
import {
configure,
AuthModel,
CalendarEmailProvider,
CALENDAR_AUTH_MESSAGE_TYPE,
} from '@blazeo.com/calendar-client';
configure({ baseUrl: 'https://your-appointment-api.example.com' });
const participantId = '...'; // logged-in participant GUID
// Optional: already connected?
const statusRes = await AuthModel.getAuthorizationStatus(participantId);
if (statusRes.status === 'success' && statusRes.data?.isAuthorized) {
// show connected UI
}
// Load provider cards (Google / Gmail, Microsoft Outlook)
const providersRes = await AuthModel.getCalendarProviders();
const providers = providersRes.data; // [{ key: 'google', displayName: 'Google / Gmail', ... }, ...]
// User clicks Google
const urlRes = await AuthModel.getAuthorizationUrl(participantId, 'google');
const popup = AuthModel.openOAuthPopup(urlRes.data.authorizationUrl);
// Recommended: waitForAuthorization races the popup's postMessage against polling
// Auth/Status, so you still get a result even if postMessage never arrives (see "Why
// waitForAuthorization / waitForDirectAuthorization" below). Closes the popup for you.
const { promise } = AuthModel.waitForAuthorization(participantId, popup);
const result = await promise;
if (result.status === 'success') {
// close modal, show success
} else {
// result.status === 'error' | 'timeout' — result.message has the detail
}
// On modal unmount, if still waiting: cancel();
email_provider for getAuthorizationUrl: google, gmail, 1, outlook, microsoft, or 2.
All methods return Promise<{ status, data?, message? }>. Check response.status === 'success' and use response.data.
Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat below)
const unsubscribe = AuthModel.onCalendarAuthMessage(async (payload) => {
if (payload.status === 'success') {
const check = await AuthModel.getAuthorizationStatus(participantId);
if (check.data?.isAuthorized) {
// close modal, show success
}
}
});
// On modal unmount: unsubscribe(); popup?.close();
Why waitForAuthorization / waitForDirectAuthorization
The OAuth popup (Authorized.html / Error.html) reports back to your app by calling
window.opener.postMessage(...). That works only while window.opener is still set. Once the
popup navigates away to Google's or Microsoft's own sign-in pages, the browser frequently clears
window.opener because of Cross-Origin-Opener-Policy — and it does not come back even when
the popup later redirects back to our domain. When that happens, postMessage is never called
and a plain onCalendarAuthMessage listener never fires, even though authorization succeeded.
waitForAuthorization and waitForDirectAuthorization fix this by also polling a status
endpoint (Auth/Status / Auth/DirectAuthorizationStatus) in the background. Whichever signal
arrives first — the postMessage or the poll — resolves the returned promise; the other is
cleaned up automatically, and the popup is closed for you. Use onCalendarAuthMessage directly
only if you need to keep your own polling/UI logic and don't mind the "postMessage may just never
fire" caveat above.
Calendar authorization (self-serve — no participant yet)
Use this instead of getAuthorizationUrl when the logged-in user has no participant record
yet and the UI should only show a Google/Outlook button — no name/email/calendar-type form.
The backend creates the participant from the provider's own account email/name once OAuth
completes, and reports the new id via the participantId field on the onCalendarAuthMessage
success payload.
import { configure, AuthModel } from '@blazeo.com/calendar-client';
configure({ baseUrl: 'https://your-appointment-api.example.com' });
const companyKey = '...'; // logged-in user's company
// User clicks "Connect Google Calendar" — no form, just the provider choice.
const startRes = await AuthModel.startDirectAuthorization(companyKey, 'google');
const popup = AuthModel.openOAuthPopup(startRes.data.authorizationUrl);
// Recommended: waitForDirectAuthorization races the popup's postMessage against polling
// Auth/DirectAuthorizationStatus by requestId, so you still get the new participantId even if
// postMessage never arrives (see "Why waitForAuthorization / waitForDirectAuthorization" above).
const { promise } = AuthModel.waitForDirectAuthorization(startRes.data.requestId, popup);
const result = await promise;
if (result.status === 'success') {
const participantId = result.participantId; // new participant created by the backend
// store participantId, close modal, show success
} else {
// result.status === 'error' | 'timeout'. On 'error', result.message may say the signed-in
// Google/Microsoft account already belongs to another participant in this company.
}
// On modal unmount, if still waiting: cancel();
startDirectAuthorization never creates or updates a participant on the client — everything
happens on the backend after the OAuth popup completes. requestId in the response is the OAuth
state value; it is not a participant id, but do keep it in memory (e.g. component state)
for the lifetime of the popup — waitForDirectAuthorization (and getDirectAuthorizationStatus)
need it to poll for the result.
Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat above)
const unsubscribe = AuthModel.onCalendarAuthMessage(async (payload) => {
if (payload.status === 'success') {
const participantId = payload.participantId; // new participant created by the backend
// store participantId, close modal, show success
} else {
// e.g. the signed-in Google/Microsoft account already belongs to another
// participant in this company — payload.message has the detail
}
});
// On modal unmount: unsubscribe(); popup?.close();
Samples
sample/– Uses the package viafile:..for local development (runnpm run buildin parent first).sample-npm/– Uses the package from npm:npm i @blazeo.com/calendar-client. For testing the published package.