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

MethodWhat 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

MethodWhat 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

MethodWhat 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

MethodWhat 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

ClassWhen thrown
NoSlotsErrorNo available slots in the requested window
CalendarNotFoundErrorNo calendar found for the owner
CalendarReauthRequiredErrorThe connected calendar credentials need owner reauthorization
LookaheadExceedsMaximumErrornextAvailableLookaheadHours exceeds 72
ArbitrationRejectedErrorBooking rejected by arbitration engine (carries alternatives[])
HoldExpiredErrorHold TTL elapsed before confirm
HoldNotFoundErrorHold ID not found
HoldAlreadyPromotedErrorHold was already confirmed
SlotOutsideHoldErrorConfirmed slot is outside the hold window
BookingNotFoundErrorBooking ID not found
BookingNotCancellableErrorBooking status does not allow cancellation
PermissionDeniedErrorAPI key lacks the required permission scope
OwnerScopeDeniedErrorOwner is outside the agent's allowed scope
UnknownApiKeyErrorAPI key not recognised
IdempotencyConflictErrorIdempotency key reused with different parameters
IdempotencyInFlightErrorA request with this key is already in flight