TypeScript SDK
@openavail/sdk is a zero-dependency TypeScript client for Node 18+. No code generation, no OpenAPI toolchain — just install and call.
The quickstart assumes an approval-mode key with read_freebusy and
create_booking_proposals. Add auto-booking or sensitive scopes only when the agent has earned that
trust.
Install
npm i @openavail/sdk
API keys
API keys are created by calendar owners or org admins in Dashboard → Agents. The human operator chooses the agent's permissions and allowed calendar owners, then supplies the key to the runtime as OPENAVAIL_API_KEY. Agents cannot create or upgrade their own keys.
Quick start
import { OpenavailClient } from '@openavail/sdk';
const client = new OpenavailClient({ apiKey: process.env.OPENAVAIL_API_KEY });
// 1. Start with context: timezone, setup warnings, feature availability, classes
const ctx = await client.getOwnerContext('alex@acme.com');
const primaryTimezone = ctx.calendars.find((c) => c.is_primary)?.timezone;
if (ctx.setupWarnings.some((w) => w.code === 'WORKING_HOURS_NOT_CONFIGURED')) {
// Openavail will allow scheduling, but no preferred booking window is enforced.
// Confirm with the calendar owner for evenings, weekends, or unusual local times.
}
// 2. Create an approval-first proposal without reserving or writing time
const proposal = await client.createBookingProposal({
ownerEmail: 'alex@acme.com',
title: 'Strategy call',
durationMinutes: 60,
meetingClass: 'external_customer_call',
requestedWindow: {
start: '2026-07-01T09:00:00Z',
end: '2026-07-01T17:00:00Z',
},
attendees: [{ email: 'alex@acme.com' }],
});
The owner reviews the proposal in Dashboard → Approvals. The agent can call
getBookingProposal(proposal.proposalId) or read pending notifications for the outcome. All
datetimes are ISO 8601 UTC. Use the owner's timezone from getOwnerContext() to convert local times
before passing them in.
Broad requestedWindow values produce a curated owner review set, not exhaustive availability. In
beta, Openavail persists up to 20 valid proposal candidates, preserves up to 3 preferred times
first, and returns curation metadata such as candidateLimit, availableValidCandidateCount, and
candidatesTruncated.
If a proposal window cannot produce any valid candidates, createBookingProposal() throws
NoSlotsError. Use err.reasonCode to distinguish busy calendars from rule failures such as
WORKING_HOURS or OFF_DAY.
Trusted auto-booking
Trusted agents can still search, hold, and confirm directly. That key needs read_freebusy,
create_holds, and create_bookings.
const { candidates } = await client.searchAvailability({
ownerEmail: 'alex@acme.com',
durationMinutes: 60,
meetingClass: 'external_customer_call',
earliestStart: '2026-07-01T09:00:00Z',
latestEnd: '2026-07-01T17:00:00Z',
});
const hold = await client.createHold({
ownerEmail: 'alex@acme.com',
meetingClass: 'external_customer_call',
holdScope: 'candidate',
candidate: candidates[0],
});
const booking = await client.confirmHold({
holdId: hold.holdId,
start: hold.heldWindow.start,
end: hold.heldWindow.end,
title: 'Strategy call',
});
Availability search is also bounded: responses default to 50 candidates, and maxResults may
request up to 100. If candidatesTruncated is true, narrow the window or run another search.
Use hold.expiresInSeconds for hold freshness and retry decisions.
Direct booking
Use createBooking() only when the slot is already known and confirmed by the user. It runs arbitration and can still reject or preempt, but it does not give a human a list of options first.
const booking = await client.createBooking({
ownerEmail: 'alex@acme.com',
meetingClass: 'external_customer_call',
start: '2026-07-01T14:00:00Z',
end: '2026-07-01T15:00:00Z',
title: 'Customer call',
attendees: [{ email: 'customer@example.com' }],
});
for (const warning of booking.warnings) {
if (warning.code === 'WORKING_HOURS_NOT_CONFIGURED') {
// The booking succeeded, but Openavail did not enforce a preferred working window.
}
}
Methods
Availability
| Method | What it does |
|---|---|
createBookingProposal(options) | Create an approval-first proposal with a curated review set, without a hold |
getBookingProposal(proposalId) | Fetch proposal status, candidates, decision, and final booking id |
searchAvailability(options) | Find capped candidate slots without reserving time |
createHold(options) | Reserve a candidate or short negotiation window |
confirmHold(options) | Commit a hold to the calendar |
createBooking(options) | Book directly without a hold — use when you already know the slot |
simulate(options) | Preview the arbitration decision without creating anything (Pro) |
Booking management
| Method | What it does |
|---|---|
listBookings(options) | List committed bookings with cursor pagination |
getBooking(bookingId) | Fetch a single booking by ID |
cancelBooking(bookingId, options?) | Cancel a booking — owner-scoped, not creator-only |
updateBooking(bookingId, options) | Update title, description, or attendees |
Calendar & configuration
| Method | What it does |
|---|---|
listCalendars(ownerEmail) | List connected calendars, primary first |
getOwnerContext(ownerEmail?) | Calendars + working hours + meeting classes in one call |
listMeetingClasses() | List valid meeting class names and priority policy |
getScheduleRules(options) | Get working hours and slot interval |
Notifications
| Method | What it does |
|---|---|
getPendingNotifications() | Fetch all unacknowledged notifications (up to 7 days) |
ackNotifications(ids) | Acknowledge notifications by ID (up to 100 per call) |
Error handling
Catch by class, not by status code. Every error class extends OpenavailError and carries code, httpStatus, and pendingNotifications.
import { ArbitrationRejectedError, NoSlotsError } from '@openavail/sdk';
try {
await client.confirmHold({ holdId, start, end, title });
} catch (err) {
if (err instanceof ArbitrationRejectedError) {
// err.alternatives — counter-proposed slots within the same hold
// err.reason — human-readable explanation
if (err.alternatives.length > 0) {
// Confirm with an alternative slot — no new availability check needed
await client.confirmHold({ holdId, start: err.alternatives[0].start, end: err.alternatives[0].end, title });
}
}
}
import { NoSlotsError } from '@openavail/sdk';
try {
await client.searchAvailability({ ..., nextAvailableLookaheadHours: 72 });
} catch (err) {
if (err instanceof NoSlotsError) {
// err.reasonCode — why: 'NO_FREE_SLOTS' | 'DAILY_HOURS_LIMIT' | 'OFF_DAY' | 'WORKING_HOURS' | 'HARD_BLOCK'
if (err.nextAvailable) {
// Nearest free slot found within the lookahead window
console.log('Next available slot starts at', err.nextAvailable.start);
} else if (err.nextAvailableExceedsLookahead) {
// Slots exist but fall beyond the lookahead window — retry with a larger value
console.log('Slots exist beyond the lookahead — retry with a wider window');
}
}
}
All error classes
| Class | When thrown |
|---|---|
NoSlotsError | No available slots in the requested window |
CalendarNotFoundError | No calendar found for the owner |
CalendarReauthRequiredError | The connected calendar credentials need owner reauthorization |
LookaheadExceedsMaximumError | nextAvailableLookaheadHours exceeds 72 |
ArbitrationRejectedError | Booking rejected by arbitration engine (carries alternatives[]) |
HoldExpiredError | Hold TTL elapsed before confirm |
HoldNotFoundError | Hold ID not found |
HoldAlreadyPromotedError | Hold was already confirmed |
SlotOutsideHoldError | Confirmed slot is outside the hold window |
BookingNotFoundError | Booking ID not found |
BookingNotCancellableError | Booking status does not allow cancellation |
PermissionDeniedError | API key lacks the required permission scope |
OwnerScopeDeniedError | Owner is outside the agent's allowed scope |
UnknownApiKeyError | API key not recognised |
IdempotencyConflictError | Idempotency key reused with different parameters |
IdempotencyInFlightError | A request with this key is already in flight |