@microboxlabs/miot-calendar-client
v0.5.0
Published
[](https://www.npmjs.com/package/@microboxlabs/miot-calendar-client) [](./LICENSE)
Readme
@microboxlabs/miot-calendar-client
TypeScript client for the ModularIoT Calendar API. Zero dependencies — uses the native fetch API.
Installation
npm install @microboxlabs/miot-calendar-clientQuick Start
A complete workflow: create a calendar, define a time window, generate slots, and book one.
import { createMiotCalendarClient } from "@microboxlabs/miot-calendar-client";
const client = createMiotCalendarClient({
baseUrl: "https://your-api-host.com",
});
// 1. Create a calendar
const calendar = await client.calendars.create({
code: "maintenance",
name: "Vehicle Maintenance",
timezone: "America/New_York", // defaults to "UTC" if omitted
});
// 2. Add a time window (Mon–Fri, 8 AM – 5 PM)
const timeWindow = await client.calendars.createTimeWindow(calendar.id, {
name: "Business Hours",
startHour: 8,
endHour: 17,
validFrom: "2026-03-01",
daysOfWeek: "1,2,3,4,5", // Mon–Fri
capacity: 2, // default: 1
});
// 3. Generate slots for the first two weeks
const result = await client.slots.generate({
calendarId: calendar.id,
startDate: "2026-03-01",
endDate: "2026-03-14", // max range: 90 days
});
console.log(`Created ${result.slotsCreated} slots`);
// 4. Find available slots
const { data: slots } = await client.slots.list({
calendarId: calendar.id, // required
available: true,
startDate: "2026-03-03",
endDate: "2026-03-07",
});
// 5. Book the first available slot
const booking = await client.bookings.create(
{
calendarId: calendar.id,
resource: { id: "truck-42", type: "vehicle", label: "Truck 42" },
slot: { date: slots[0].slotDate, hour: slots[0].slotHour, minutes: slots[0].slotMinutes },
},
{ userId: "user-abc" }, // sets X-User-Id header → stored as createdBy
);Configuration
const client = createMiotCalendarClient({
baseUrl: "https://your-api-host.com", // Required — API base URL
headers: { Authorization: "Bearer ..." }, // Optional — merged into every request
fetch: customFetch, // Optional — custom fetch implementation
});| Option | Type | Required | Description |
|--------|------|----------|-------------|
| baseUrl | string | Yes | Base URL of the Calendar API |
| headers | Record<string, string> | No | Default headers sent with every request |
| fetch | typeof fetch | No | Custom fetch implementation (defaults to globalThis.fetch) |
API Reference
Calendars
calendars.list(params?)
List all calendars, optionally filtered by active status or group membership.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| active | boolean | No | Filter by active status |
| groupCode | string | No | Filter calendars belonging to this group code |
Returns: CalendarResponse[]
calendars.get(id)
Get a single calendar by ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Calendar ID |
Returns: CalendarResponse
Throws: MiotCalendarApiError with status 404 if not found.
calendars.create(body)
Create a new calendar.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| code | string | Yes | — | Unique code identifier |
| name | string | Yes | — | Display name |
| description | string | No | — | Optional description |
| timezone | string | No | "UTC" | IANA timezone |
| active | boolean | No | true | Whether the calendar is active |
| parallelism | number | No | 1 | Parallel resources per slot (e.g. loading docks) |
| groups | string[] | No | — | Group codes to assign. null = no change; [] = remove all; ["code"] = replace all |
Returns: CalendarResponse
calendars.update(id, body)
Replace a calendar's fields. Takes the same body as create.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Calendar ID |
| body | CalendarRequest | Yes | Updated calendar data (include groups to reassign group membership) |
Returns: CalendarResponse
calendars.deactivate(id)
Deactivate a calendar (soft delete).
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Calendar ID |
Returns: void (HTTP 204 — no content)
Time Windows
Time windows are managed through the calendars namespace.
calendars.listTimeWindows(calendarId)
List all time windows for a calendar.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| calendarId | string | Yes | Calendar ID |
Returns: TimeWindowResponse[]
calendars.createTimeWindow(calendarId, body)
Create a time window within a calendar.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| name | string | Yes | — | Time window name |
| startHour | number | Yes | — | Start hour (0–23) |
| endHour | number | Yes | — | End hour (0–23, must be > startHour) |
| validFrom | string | Yes | — | Start date (YYYY-MM-DD) |
| validTo | string | No | — | End date (YYYY-MM-DD) |
| capacity | number | No | 1 | Total number of services this window can handle across all slots |
| daysOfWeek | string | No | — | Comma-separated days (1=Mon … 7=Sun) |
| active | boolean | No | true | Whether the time window is active |
Returns: TimeWindowResponse
calendars.updateTimeWindow(calendarId, timeWindowId, body)
Update a time window. Takes the same body as createTimeWindow.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| calendarId | string | Yes | Calendar ID |
| timeWindowId | string | Yes | Time window ID |
| body | TimeWindowRequest | Yes | Updated time window data |
Returns: TimeWindowResponse
Groups
Calendar groups let you organize calendars into named collections. Calendars can belong to multiple groups; use CalendarRequest.groups on create or update to manage membership.
groups.list(params?)
List all calendar groups, optionally filtered by active status.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| active | boolean | No | Filter by active status |
Returns: CalendarGroupResponse[]
groups.get(id)
Get a single calendar group by ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Group ID |
Returns: CalendarGroupResponse
Throws: MiotCalendarApiError with status 404 if not found.
groups.create(body)
Create a new calendar group.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| code | string | Yes | — | Unique code identifier (max 50 chars) |
| name | string | Yes | — | Display name (max 255 chars) |
| description | string | No | — | Optional description |
| active | boolean | No | true | Whether the group is active |
Returns: CalendarGroupResponse
Throws: MiotCalendarApiError with status 400 if the code is already taken.
groups.update(id, body)
Replace a calendar group's fields. Takes the same body as create.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Group ID |
| body | CalendarGroupRequest | Yes | Updated group data |
Returns: CalendarGroupResponse
groups.deactivate(id)
Deactivate a calendar group (soft delete). Calendars in the group are not affected.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Group ID |
Returns: void (HTTP 204 — no content)
Slots
slots.list(params)
List slots for a calendar. The calendarId parameter is required.
| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| calendarId | string | Yes | — | Calendar ID |
| available | boolean | No | — | Filter by availability |
| startDate | string | No | today | Start of date range (YYYY-MM-DD) |
| endDate | string | No | today + 7 days | End of date range (YYYY-MM-DD) |
Returns: SlotListResponse — { data: SlotResponse[], total: number }
slots.get(id)
Get a single slot by ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot ID |
Returns: SlotResponse
Throws: MiotCalendarApiError with status 404 if not found.
slots.generate(body)
Generate slots for a calendar based on its time windows.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| calendarId | string | Yes | Calendar ID |
| startDate | string | Yes | Start date (YYYY-MM-DD) |
| endDate | string | Yes | End date (YYYY-MM-DD) |
Note: The date range must not exceed 90 days. The API returns
400 Bad Requestif the range is larger.
Returns: GenerateSlotsResponse — { slotsCreated: number, slotsSkipped: number, message: string }
slots.updateStatus(id, body)
Manually change a slot's status.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot ID |
| status | "OPEN" \| "CLOSED" | Yes | New status |
Note: Only
OPENandCLOSEDare accepted.FULLis managed automatically by the system based on occupancy and cannot be set manually. PassingFULLreturns400 Bad Request.
Returns: SlotResponse
Slot Managers
Slot managers automate slot generation for calendars. Each manager keeps slots generated a configurable number of days in advance and can be triggered manually or by an external scheduler (e.g. K8s CronJob).
slotManagers.list(params?)
List all slot managers, optionally filtered by active status.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| active | boolean | No | When true, return only active managers |
Returns: SlotManagerResponse[]
slotManagers.create(body)
Create an automatic slot generation manager for a calendar.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| calendarId | string | Yes | — | Calendar ID to manage |
| active | boolean | No | true | Enable or disable automatic generation |
| daysInAdvance | number | No | 30 | Days ahead to keep slots generated |
| batchDays | number | No | 7 | Max days to generate per scheduler run |
| reprocessFrom | string | No | — | Force regeneration from this date (YYYY-MM-DD) |
| reprocessTo | string | No | — | Force regeneration through this date (required with reprocessFrom) |
Returns: SlotManagerResponse
Throws: MiotCalendarApiError with status 400 if invalid or duplicate.
slotManagers.get(id)
Get a slot manager by ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot manager ID |
Returns: SlotManagerResponse
Throws: MiotCalendarApiError with status 404 if not found.
slotManagers.update(id, body)
Update a slot manager's configuration. Takes the same body as create. Set reprocessFrom + reprocessTo to schedule one-shot slot regeneration.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot manager ID |
| body | SlotManagerRequest | Yes | Updated configuration |
Returns: SlotManagerResponse
slotManagers.deactivate(id)
Deactivate a slot manager (soft delete — disables automatic generation without removing history).
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot manager ID |
Returns: void (HTTP 204 — no content)
slotManagers.runAll()
Synchronously run all active slot managers. Returns when all managers have completed.
Returns: SlotManagerRunResponse[]
slotManagers.run(id)
Synchronously run a single slot manager and return its run record.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Slot manager ID |
Returns: SlotManagerRunResponse
Throws: MiotCalendarApiError with status 404 if not found.
slotManagers.listAllRuns(params?)
List recent runs across all managers.
| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| limit | number | No | 50 | Maximum number of records to return |
Returns: SlotManagerRunResponse[]
slotManagers.listRuns(id, params?)
List runs for a specific manager.
| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | string | Yes | — | Slot manager ID |
| limit | number | No | 20 | Maximum number of records to return |
Returns: SlotManagerRunResponse[]
Throws: MiotCalendarApiError with status 404 if manager not found.
Bookings
bookings.list(params?)
List bookings, optionally filtered by calendar and date range.
| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| calendarId | string | No | — | Filter by calendar |
| startDate | string | No | today | Start of date range (YYYY-MM-DD) |
| endDate | string | No | today + 30 days | End of date range (YYYY-MM-DD) |
Returns: BookingListResponse — { data: BookingResponse[], total: number }
bookings.get(id)
Get a single booking by ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Booking ID |
Returns: BookingResponse
Throws: MiotCalendarApiError with status 404 if not found.
bookings.create(body, options?)
Create a booking for a specific slot.
Body (BookingRequest):
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| calendarId | string | Yes | Calendar ID |
| resource | ResourceData | Yes | The resource being booked |
| slot | SlotData | Yes | Target slot (date + time) |
Options:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| userId | string | No | Sets X-User-Id header — stored as createdBy in the response |
Returns: BookingResponse
Throws:
404— Calendar or slot not found409— Slot is full (no available capacity)
bookings.cancel(id)
Cancel a booking.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | Yes | Booking ID |
Returns: void (HTTP 204 — no content)
bookings.listByResource(resourceId)
List all bookings for a specific resource.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| resourceId | string | Yes | Resource ID |
Returns: BookingListResponse
Types
CalendarGroupRequest
interface CalendarGroupRequest {
code: string; // Unique code identifier (max 50 chars)
name: string; // Display name (max 255 chars)
description?: string; // Optional description
active?: boolean; // Active status (default: true)
}CalendarGroupResponse
interface CalendarGroupResponse {
id: string;
code: string;
name: string;
description?: string;
active: boolean;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}CalendarRequest
interface CalendarRequest {
code: string; // Unique code identifier
name: string; // Display name
description?: string; // Optional description
timezone?: string; // IANA timezone (default: "UTC")
active?: boolean; // Active status (default: true)
parallelism?: number; // Parallel resources per slot (default: 1, e.g. loading docks)
groups?: string[]; // Group codes to assign. null = no change; [] = remove all; ["code"] = replace all
}CalendarResponse
interface CalendarResponse {
id: string;
code: string;
name: string;
description?: string;
timezone: string; // Always present (default: "UTC")
active: boolean; // Always present (default: true)
parallelism: number; // Parallel resources per slot (min: 1)
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
groups?: CalendarGroupResponse[]; // Groups this calendar belongs to
}TimeWindowRequest
interface TimeWindowRequest {
name: string; // Time window name
startHour: number; // 0–23
endHour: number; // 0–23 (must be > startHour)
validFrom: string; // YYYY-MM-DD
validTo?: string; // YYYY-MM-DD
capacity?: number; // Default: 1 — total services this window can handle
daysOfWeek?: string; // Comma-separated: "1,2,3,4,5" (1=Mon, 7=Sun)
active?: boolean; // Default: true
}TimeWindowResponse
interface TimeWindowResponse {
id: string;
calendarId: string;
name: string;
startHour: number;
endHour: number;
slotDurationMinutes: number; // Read-only, derived from capacity model
capacity: number; // Total services this window can handle
daysOfWeek: string;
validFrom: string;
validTo?: string;
active: boolean; // Always present (default: true)
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}SlotResponse
interface SlotResponse {
id: string;
calendarId: string;
timeWindowId?: string; // May be absent for manually created slots
slotDate: string; // YYYY-MM-DD
slotHour: number; // 0–23
slotMinutes: number; // 0–59
capacity: number; // Total capacity
currentOccupancy: number; // Current bookings count
availableCapacity: number; // capacity - currentOccupancy
status: SlotStatus; // "OPEN" | "FULL" | "CLOSED"
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}SlotListResponse
interface SlotListResponse {
data: SlotResponse[];
total: number;
}GenerateSlotsRequest
interface GenerateSlotsRequest {
calendarId: string;
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD (max 90 days from startDate)
}GenerateSlotsResponse
interface GenerateSlotsResponse {
slotsCreated: number;
slotsSkipped: number;
message: string;
}UpdateSlotStatusRequest
interface UpdateSlotStatusRequest {
status: SlotStatus; // Only "OPEN" or "CLOSED" accepted
}SlotManagerRequest
interface SlotManagerRequest {
calendarId: string; // Calendar ID to manage
active?: boolean; // Enable/disable (default: true)
daysInAdvance?: number; // Days ahead to keep generated (default: 30)
batchDays?: number; // Max days per scheduler run (default: 7)
reprocessFrom?: string; // YYYY-MM-DD — one-shot regeneration start
reprocessTo?: string; // YYYY-MM-DD — one-shot regeneration end
}SlotManagerResponse
interface SlotManagerResponse {
id: string;
calendarId: string;
calendarCode: string;
calendarName: string;
active: boolean;
daysInAdvance: number;
batchDays: number;
reprocessFrom?: string; // Cleared after successful run
reprocessTo?: string; // Cleared after successful run
lastRunAt?: string; // ISO 8601
lastRunStatus?: string; // IDLE, RUNNING, SUCCESS, FAILED, SKIPPED
lastRunError?: string; // Error message from last failed run
generatedThrough?: string; // YYYY-MM-DD — latest date with generated slots
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}SlotManagerRunResponse
interface SlotManagerRunResponse {
id: string;
managerId: string;
triggeredBy: string; // SCHEDULER, API, CLI
startedAt: string; // ISO 8601
finishedAt?: string; // ISO 8601
status: string; // RUNNING, SUCCESS, FAILED, SKIPPED
slotsCreated: number;
slotsSkipped: number;
generatedFrom?: string; // YYYY-MM-DD
generatedThrough?: string; // YYYY-MM-DD
errorMessage?: string;
}BookingRequest
interface BookingRequest {
calendarId: string;
resource: ResourceData;
slot: SlotData;
}BookingResponse
interface BookingResponse {
id: string;
calendarId: string;
resource: ResourceData;
slot: SlotData;
createdAt: string; // ISO 8601
createdBy?: string; // Set via X-User-Id header on create
}BookingListResponse
interface BookingListResponse {
data: BookingResponse[];
total: number;
}ResourceData
interface ResourceData {
id: string; // Resource identifier
type?: string; // Resource type (e.g. "vehicle", "room")
label?: string; // Human-readable label
data?: Record<string, unknown>; // Arbitrary metadata
}SlotData
interface SlotData {
date: string; // YYYY-MM-DD
hour: number; // 0–23
minutes: number; // 0–59
}SlotStatus
type SlotStatus = "OPEN" | "FULL" | "CLOSED";ErrorResponse
interface ErrorResponse {
error: string; // Error type (e.g. "Bad Request")
message: string; // Detailed error message
status: number; // HTTP status code
timestamp: string; // ISO 8601
}ClientConfig
interface ClientConfig {
baseUrl: string;
headers?: Record<string, string>;
fetch?: typeof fetch;
}Error Handling
All API errors throw MiotCalendarApiError.
import { MiotCalendarApiError } from "@microboxlabs/miot-calendar-client";
try {
await client.bookings.create({
calendarId: "cal-123",
resource: { id: "truck-42", type: "vehicle", label: "Truck 42" },
slot: { date: "2026-03-01", hour: 10, minutes: 0 },
});
} catch (error) {
if (error instanceof MiotCalendarApiError) {
console.log(error.status); // HTTP status code (e.g. 409)
console.log(error.message); // Error message string
console.log(error.body); // ErrorResponse object or raw string
}
}Error Codes
| Status | Meaning | Common Causes |
|--------|---------|---------------|
| 400 | Bad Request | Validation errors, slot generation range > 90 days, setting status to FULL manually |
| 404 | Not Found | Calendar, slot, or booking does not exist |
| 409 | Conflict | Slot is full (no available capacity for booking) |
Error Body
The body property on MiotCalendarApiError can be either:
- An
ErrorResponseobject witherror,message,status, andtimestampfields — returned when the API sends a JSON error response. - A plain
string— returned as a fallback when the API response is not valid JSON (e.g. gateway errors, load balancer timeouts).
if (error instanceof MiotCalendarApiError) {
if (typeof error.body === "string") {
// Non-JSON error (gateway, timeout, etc.)
console.log("Raw error:", error.body);
} else {
// Structured API error
console.log(error.body.error); // "Conflict"
console.log(error.body.message); // "Slot is full"
console.log(error.body.timestamp); // "2026-03-01T10:00:00Z"
}
}Custom Fetch
Pass a custom fetch implementation for server-side rendering, auth interceptors, or testing.
Auth interceptor
const client = createMiotCalendarClient({
baseUrl: "https://your-api-host.com",
fetch: async (input, init) => {
const token = await getAccessToken(); // your auth logic
const headers = new Headers(init?.headers);
headers.set("Authorization", `Bearer ${token}`);
return globalThis.fetch(input, { ...init, headers });
},
});Testing with a mock fetch
import { createMiotCalendarClient } from "@microboxlabs/miot-calendar-client";
const mockFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
return new Response(JSON.stringify({ id: "cal-1", code: "test", name: "Test" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const client = createMiotCalendarClient({
baseUrl: "https://mock-api",
fetch: mockFetch,
});
const calendar = await client.calendars.get("cal-1");