npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openavail/sdk

v1.2.2

Published

TypeScript client for the [Openavail](https://openavail.com) API. Zero runtime dependencies, Node 18+, native `fetch`.

Readme

@openavail/sdk

TypeScript client for the Openavail API. Zero runtime dependencies, Node 18+, native fetch.

Install

npm i @openavail/sdk

Quick start

import { OpenavailClient } from '@openavail/sdk';

const client = new OpenavailClient({ apiKey: process.env.OPENAVAIL_API_KEY });

// 1. Call getOwnerContext first — timezone, setup warnings, unavailable features, and valid classes
const ctx = await client.getOwnerContext('[email protected]');
const tz  = ctx.calendars.find(c => c.is_primary)?.timezone ?? 'UTC';
const missingHours = ctx.setupWarnings.some(w => w.code === 'WORKING_HOURS_NOT_CONFIGURED');

// 2. Approval mode: propose a booking without reserving or writing time
const proposal = await client.createBookingProposal({
  ownerEmail:    '[email protected]',
  title:         'Strategy call',
  durationMinutes: 60,
  meetingClass:  'Critical',
  requestedWindow: {
    start: '2026-07-01T09:00:00Z',
    end:   '2026-07-01T17:00:00Z',
  },
  attendees: [{ email: '[email protected]' }],
});

console.log('Awaiting owner approval:', proposal.proposalId);

latestEnd is a deadline, not a start boundary. For a 60-min meeting starting at 2 pm, set latestEnd to at least 3 pm — the meeting must fully end within it.

Getting an API key

  1. Log in to the Openavail dashboard as the calendar owner or org admin.
  2. Go to Agents → Register agent and create an agent with the permissions and owner scope it needs.
  3. Click Create API key under the agent.
  4. Copy the key immediately — it is not shown again. Keys are prefixed ak_.

For a new approval-mode agent, grant read_freebusy and create_booking_proposals. For a trusted auto-booking agent, grant read_freebusy, create_holds, and create_bookings. Grant read_events only when the agent should see booking titles, descriptions, and attendees in Openavail responses. Grant preempt only to trusted agents that may displace lower-priority bookings when rules allow it.

Using the SDK alongside the MCP server? The MCP server reads OPENAVAIL_API_KEY from its own process environment (configured via your MCP client, e.g. ~/.claude.json). That key is not automatically available in your shell session. Set OPENAVAIL_API_KEY in your shell profile separately if you also want to use the SDK directly.

Client

const client = new OpenavailClient({
  apiKey: string,    // required
  baseUrl?: string,  // defaults to https://api.openavail.com
})

Public scheduling auth

Openavail has two separate credential types:

  • OpenavailClient({ apiKey }) uses an owner-agent API key (ak_...). It acts inside the key owner's Openavail account and is for private owner-scoped APIs such as availability, holds, bookings, and private booking proposals.
  • OpenavailPublicSchedulingClient({ requesterCredential }) uses an optional requester credential (rc_...). It proves who an external requester is when asking another owner for time through /public. It does not grant calendar access.

Anonymous public scheduling does not need a credential. Verified requester flows use a requester credential only to identify the requester; the target owner's public scheduling policy still decides visibility, review, and auto-book behavior.

import {
  OpenavailPublicSchedulingAdminClient,
  OpenavailPublicSchedulingClient,
} from '@openavail/sdk';

// Owner/admin setup: uses an owner-agent API key.
const admin = new OpenavailPublicSchedulingAdminClient({
  apiKey: process.env.OPENAVAIL_API_KEY!,
});

const [boundary] = await admin.listBoundaries();
const meetingTypes = boundary ? await admin.listMeetingTypes(boundary.id) : [];

// External requester flow: anonymous or requester credential.
const publicScheduling = new OpenavailPublicSchedulingClient({
  requesterCredential: process.env.OPENAVAIL_REQUESTER_CREDENTIAL,
});

const visibleTypes = await publicScheduling.listMeetingTypes('psch_abc123');

for (const type of visibleTypes) {
  console.log(type.name, type.durationMinutes, type.suggestedTimes);
}

Methods

Public scheduling

Anonymous free-text proposal

const publicScheduling = new OpenavailPublicSchedulingClient();

const proposal = await publicScheduling.createBookingProposal({
  boundaryId: 'psch_abc123',
  message: 'I need 30 minutes next week to discuss a potential partnership.',
  requesterContact: { email: '[email protected]' },
  attendees: [{ email: '[email protected]' }],
});

console.log(proposal.statusUrl);
console.log(proposal.contactVerificationUrl); // anonymous requests confirm one contact email

requesterContact is the person submitting the request. attendees is the proposed attendee list and should include the requester. If a public meeting type has an attendee cap of 1, send only the requester.

Verified requester structured proposal

const publicScheduling = new OpenavailPublicSchedulingClient({
  requesterCredential: process.env.OPENAVAIL_REQUESTER_CREDENTIAL!,
});

const proposal = await publicScheduling.createBookingProposal({
  boundaryId: 'psch_abc123',
  publicMeetingType: 'customer_escalation',
  durationMinutes: 30,
  requestedWindow: {
    start: '2026-07-01T09:00:00Z',
    end: '2026-07-08T17:00:00Z',
  },
  requesterContact: { email: '[email protected]', name: 'Acme Coordinator' },
  attendees: [{ email: '[email protected]', name: 'Person Name' }],
  reason: 'Production incident follow-up',
});

Use listMeetingTypes() first. It returns requester-visible options with publicMeetingType, display name, optional description, durationMinutes, and suggestedTimes. Suggested times are not holds; if you submit one as requestedWindow, Openavail rechecks that window before the request continues.

Polling and withdrawal

const status = await publicScheduling.getBookingProposalStatus('pat_public_access_token');

if (status.status === 'pending_review') {
  await publicScheduling.withdrawBookingProposal('pat_public_access_token');
}

When contactVerificationUrl is present, extract the contact verification token from that URL and call confirmRequesterContact(token) after the requester confirms ownership of the email address. The status token is public-safe: it never returns internal proposal IDs, private conflicts, audit rows, or raw owner availability.

Owner/admin public scheduling setup

const admin = new OpenavailPublicSchedulingAdminClient({
  apiKey: process.env.OPENAVAIL_API_KEY!,
});

const domain = await admin.createVerifiedDomain('acme.com');
console.log(domain.txtName, domain.txtValue);

const checked = await admin.checkVerifiedDomain(domain.id);
if (checked.status !== 'verified') {
  throw new Error('Add the DNS TXT record before creating a domain-bound identity');
}

const identity = await admin.createRequesterIdentity({
  displayName: 'Acme scheduling agent',
  verifiedDomainId: checked.id,
});

const meetingType = await admin.createMeetingType({
  boundaryId: 'public-boundary-id',
  name: 'Customer escalation',
  description: 'Urgent customer incidents that need the support lead.',
  meetingClassId: 'meeting-class-id',
  durationMinutes: 30,
  candidatePreview: true, // dashboard copy: "Show suggested times"
  status: 'published',
});
console.log(meetingType.publicMeetingType); // "customer-escalation"; derived from name

const issued = await admin.issueRequesterCredential({
  requesterIdentityId: identity.id,
  displayName: 'production',
});
console.log(issued.requesterCredential); // shown once; store it as OPENAVAIL_REQUESTER_CREDENTIAL

const credentials = await admin.listRequesterCredentials(identity.id);
await admin.revokeRequesterCredential(credentials[0]!.id);

createMeetingType accepts an optional publicMeetingType override, but most owner/admin flows should pass only name; the SDK derives the URL/API-safe identifier automatically.

Admin meeting type fields mirror the API, so the SDK still uses candidatePreview. In the dashboard this is labeled Show suggested times. Suggested times are cached for requester UX and are always rechecked before booking or approval.

Booking proposals

createBookingProposal(options)

Create an approval-first proposal. This discovers candidates without creating a hold or calendar event. The owner approves or rejects in the dashboard. Broad requested windows produce a curated review set, not exhaustive availability; in beta, Openavail persists up to 20 valid proposal candidates and returns curation metadata.

const proposal = await client.createBookingProposal({
  ownerEmail?: string,
  calendarType?: 'work' | 'personal' | 'other',
  title: string,
  description?: string,
  meetingClass: string,
  durationMinutes: number,
  attendees?: { email: string; displayName?: string }[],
  requestedWindow: { start: string; end: string },
  preferredTimes?: { start: string; end: string }[], // max 3
});
// → BookingProposal

getBookingProposal(proposalId)

Fetch proposal status, candidates, owner decision, and final booking id when booked.

Availability

searchAvailability(options)

Find candidate slots without reserving time. All times are ISO 8601 UTC.

const result = await client.searchAvailability({
  ownerEmail?:                string,   // required unless user-scoped key
  durationMinutes:            number,
  meetingClass:               string,
  earliestStart:              string,   // earliest the meeting may begin
  latestEnd:                  string,   // latest the meeting may END (not start)
  calendarType?:              'work' | 'personal' | 'other',
  nextAvailableLookaheadHours?: number, // default 72h, max 72h
  maxResults?:                number,   // default 50, max 100
  idempotencyKey?:            string,   // auto-generated if omitted
});
// → { requestedWindow, candidates, resolvedCalendarType, warnings, pendingNotifications, candidatesTruncated }

createHold(options)

Reserve Openavail capacity for one exact candidate or a short negotiation window.

const hold = await client.createHold({
  ownerEmail?: string,
  meetingClass: string,
  holdScope: 'candidate',
  candidate: { start: string, end: string },
  idempotencyKey?: string,
});
// → { holdId, holdScope, heldWindow, expiresAt, expiresInSeconds, resolvedCalendarType }

Use expiresInSeconds for hold freshness and retry decisions. expiresAt is an absolute UTC timestamp for logging, display, and correlation.

confirmHold(options)

Commit a hold to the calendar.

const result = await client.confirmHold({
  holdId:          string,
  start:           string,   // must fall within the hold window
  end:             string,
  title:           string,
  description?:    string,
  attendees?:      { email: string; displayName?: string }[],
  idempotencyKey?: string,
});
// → { bookingId, correlationId, start, end, title, description, calendarType, attendees,
//     displacedCount, displacedBookings, status, pendingNotifications }

simulate(options)

Preview the arbitration decision without creating a hold. Pro plan only.

const result = await client.simulate({
  ownerEmail?:  string,
  start:        string,
  end:          string,
  meetingClass: string,
  calendarType?: 'work' | 'personal' | 'other',
});
// → { decision, reason, alternatives, engineTrace, pendingNotifications }

Direct booking

createBooking(options)

Skip the hold when you already know the exact slot.

const result = await client.createBooking({
  ownerEmail?:   string,
  start:         string,
  end:           string,
  meetingClass:  string,
  title:         string,
  description?:  string,
  calendarType?: 'work' | 'personal' | 'other',
  attendees?:    { email: string; displayName?: string }[],
  idempotencyKey?: string,
});
// → BookingResult (same shape as confirmHold)

Booking management

listBookings(options)

List committed bookings with cursor pagination.

const result = await client.listBookings({
  ownerEmail?:    string,
  start?:         string,  // default: now
  end?:           string,  // default: now + 3 days
  calendarType?:  string,
  query?:         string,  // title text search
  attendeeEmail?: string,
  limit?:         number,  // max 100, default 50
  cursor?:        string,
});
// → { bookings, nextCursor, pendingNotifications }

getBooking(bookingId)

Fetch a single booking by ID.

cancelBooking(bookingId, options?)

Cancel a booking. Any agent with create_bookings permission and owner scope can cancel — not only the agent that created it.

await client.cancelBooking(bookingId, { idempotencyKey?: string });
// → { bookingId, correlationId, pendingNotifications }

updateBooking(bookingId, options)

Update a booking's metadata. To change the time, cancel and rebook.

await client.updateBooking(bookingId, {
  title?:       string,
  description?: string,
  attendees?:   { email: string; displayName?: string }[],
});

Calendar & configuration

getOwnerContext(ownerEmail?)

Calendars, working hours, and meeting classes in one call. Call this first — it gives you the owner's timezone for UTC conversion, valid meeting class names, and working hours.

listCalendars(ownerEmail)

List connected calendars for an owner, primary first.

listMeetingClasses()

List valid meeting class names and their priority tier and preempt policy. At least one active meeting class must exist at all times — agents cannot book without one, and the API rejects attempts to delete the last class.

getScheduleRules(options)

Get working hours and slot interval for an owner.

Notifications

getPendingNotifications()

Fetch all unacknowledged notifications (up to 7 days old).

ackNotifications(ids)

Acknowledge notifications by ID. Up to 100 IDs per call.

Error handling

The SDK throws typed errors — catch by class, not status code.

import { ArbitrationRejectedError, NoSlotsError } from '@openavail/sdk';

try {
  await client.confirmHold({ holdId, start, end, title });
} catch (err) {
  if (err instanceof ArbitrationRejectedError) {
    // err.reason      — why it was rejected
    // err.alternatives — counter-proposed slots (confirm with the same holdId)
    // err.nextAvailable — nearest free slot (use when alternatives is empty)
  }
}

try {
  await client.searchAvailability({ ... });
} catch (err) {
  if (err instanceof NoSlotsError) {
    // err.reasonCode — 'NO_FREE_SLOTS' | 'DAILY_HOURS_LIMIT' | 'OFF_DAY' | 'WORKING_HOURS' | 'HARD_BLOCK'
    // err.nextAvailable — nearest free slot within the lookahead window (undefined if none found)
    // err.nextAvailableExceedsLookahead — true when slots exist but fall beyond the lookahead window;
    //   retry with a larger nextAvailableLookaheadHours (max 72h)
  }
}

All errors extend OpenavailError and carry:

  • code — machine-readable string (e.g. ARBITRATION_REJECTED)
  • httpStatus — HTTP status code
  • pendingNotifications — any notifications that arrived with the error response

Error classes

| Class | When thrown | |---|---| | NoSlotsError | No available slots in the window. .reasonCode names why; .nextAvailable points to the nearest opening; .nextAvailableExceedsLookahead signals slots exist beyond the search window. | | WindowTooNarrowError | Window is shorter than the meeting duration. .windowDurationMinutes and .requiredDurationMinutes show the gap. | | WorkingHoursNotConfiguredError | Legacy compatibility for older APIs. Current availability responses return a WORKING_HOURS_NOT_CONFIGURED warning with slots instead of throwing this error. | | CalendarNotFoundError | No calendar found for the owner | | LookaheadExceedsMaximumError | nextAvailableLookaheadHours > 72 | | ArbitrationRejectedError | Booking rejected by arbitration engine. Carries alternatives, reason, and nextAvailable. | | 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 | Request with this key is already in flight |

MCP server

Prefer the MCP interface? Use @openavail/mcp — an stdio MCP server that wraps this SDK and exposes all tools under Google Calendar MCP-compatible names.