# @blazeo.com/calendar-client

> Blazeo Calendar / Appointment API client with MobX State Tree models

Latest version **1.0.103** (published 2026-10-05) · UNLICENSED license · 0 weekly downloads

## Install

```sh
npm install @blazeo.com/calendar-client
pnpm add @blazeo.com/calendar-client
yarn add @blazeo.com/calendar-client
bun add @blazeo.com/calendar-client
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 1.0.103 |
| Published | 2026-10-05 |
| First published | 2026-02-24 |
| Weekly downloads | 0 |
| License | UNLICENSED |
| TypeScript types | none |
| Module format | ESM + CommonJS |
| Node | >=18 |
| Dependencies | 2 |
| Unpacked size | 438.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Blazeo |
| Maintainers | asad-blazeo, blazeo, usmanhussain, umeruxd.breakthru |
| Keywords | blazeo, calendar, appointment, booking, mobx, mobx-state-tree |

## Links

- npm: https://www.npmjs.com/package/@blazeo.com/calendar-client
- Repository: https://github.com/blazeo-com/calendar-client
- Homepage: https://github.com/blazeo-com/calendar-client#readme
- Issues: https://github.com/blazeo-com/calendar-client/issues
- npm.io page: https://npm.io/package/@blazeo.com/calendar-client

## Dependencies (2)

- [mobx](https://npm.io/package/mobx.md) ^6.10.0
- [mobx-state-tree](https://npm.io/package/mobx-state-tree.md) ^5.4.0

## Alternatives

- [@reckona/mreact-store](https://npm.io/package/@reckona/mreact-store.md) — 976 weekly downloads
- [regular-state](https://npm.io/package/regular-state.md) — 410 weekly downloads
- [@pacote/flux-actions](https://npm.io/package/@pacote/flux-actions.md) — 65 weekly downloads
- [@pilotlab/lux-debug](https://npm.io/package/@pilotlab/lux-debug.md) — 39 weekly downloads
- [vue-persist-state](https://npm.io/package/vue-persist-state.md) — 19 weekly downloads

## Recent versions

- 1.0.103 (latest) — 2026-10-05
- 1.0.102 — 2026-10-02
- 1.0.100 — 2026-10-01
- 1.0.99 — 2026-09-29
- 1.0.98 — 2026-09-23
- 1.0.97 — 2026-09-21
- 1.0.96 — 2026-09-14
- 1.0.95 — 2026-09-11
- 1.0.94 — 2026-09-10
- 1.0.93 — 2026-09-09
- 1.0.92 — 2026-09-08
- 1.0.91 — 2026-09-08
- 1.0.90 — 2026-08-31
- 1.0.89 — 2026-08-28
- 1.0.88 — 2026-08-20
- … 83 more at https://npm.io/package/@blazeo.com/calendar-client/versions

## README

# @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):

```bash
npm install @blazeo.com/calendar-client
```

Or link locally:

```bash
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:

```js
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.

```js
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/cancel` requires a bearer token (`setAccessToken`), like `delete`. The token, or the
  `X-User-Email` / `X-User-Display-Name` / `X-User-Id` headers a service token forwards, is what the
  AppointmentCanceled activity entry records as `handledBy`.
  `cancel` / `delete` resolve to the API envelope whose `data` is an `EventCancellationData`: besides `EventId` and
  `Status` it carries the appointment's `Source` / `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 the
  `event/cancel` / `event/delete` Consumer webhooks.
- **ActivityLogModel:** `get` (`GET /activitylog/get`). Each entry exposes `source` / `leadSource`, `location` /
  `customLocation` / `calendarLocationValue`, `handledBy` (who performed the action) and `host` (the appointment's
  participant) for scheduled, rescheduled and canceled appointment activities. Canceled entries also carry
  `cancellationReason`, and their `summary` is that reason when one was given (booking notes otherwise).
- **FlowModel:** Same pattern as Calendar — `FlowModel.create({}, { env })` with no fields; static `get`, `getRaw`, `list`, `createFlow`, `updateFlow`, `delete`, `duplicate`, appearance/embed/public/preview helpers; instance methods mirror those using `flowId` on the snapshot
- **LeadModel:** `LeadModel.create({}, { env })`; static `get`, `getRaw`, `getByEmail`, `getByPhone`, `getByCompany`, `searchByCompany`, `saveColumnSelection`, `getColumnSelection`, `allowedColumns`, `requestExport`; instance `get`, `getByEmail`, `getByPhone`, `getByCompany`, `searchByCompany`, `saveColumnSelection`, `getColumnSelection`, `requestLeadExport`. Pass `{ all: true }` to `getByEmail` to include soft-deleted leads (`GET /lead/getbyemail?all`). Pass `userId` in `getByCompany(companyKey, { userId })` to return only columns saved for that user. `filters` on `first_name`, `last_name`, and `email` that share the same text match when any of those fields contains it; other filters are combined together. `searchByCompany` calls `GET /lead/company/search` with the same arguments and returns a lead when any filter matches. For `requestExport`, pass `userEmail` (or rely on JWT email claim) so the server can publish a `NotificationGenerated` message when export completes.
- **ParticipantModel:** static `get`, `getByEmail`, `getByIds`, `getAll`, …; instance `getByEmail` uses `email` + `companyKey` on the snapshot (see `GET /participant/getbyemail`)
- **AuthModel (calendar OAuth / Connect Calendar):** `getCalendarProviders`, `getAuthorizationUrl`, `startDirectAuthorization`, `getAuthorizationStatus`, `getDirectAuthorizationStatus`, `openOAuthPopup`, `onCalendarAuthMessage`, `waitForAuthorization`, `waitForDirectAuthorization` — see [Calendar authorization flow](#calendar-authorization-direct-ui)
- **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:

```js
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:

```js
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:

```text
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:

```js
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`.

<details>
<summary>Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat below)</summary>

```js
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();
```

</details>

### 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.

```js
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.

<details>
<summary>Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat above)</summary>

```js
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();
```

</details>

## Samples

- **`sample/`** – Uses the package via `file:..` for local development (run `npm run build` in parent first).
- **`sample-npm/`** – Uses the package from npm: `npm i @blazeo.com/calendar-client`. For testing the published package.

---
_Source: https://npm.io/package/@blazeo.com/calendar-client · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
