SapanCalDeveloper Docs

Node.js SDK

Install and use the bentocal package for Node.js, Bun, Deno and edge runtimes: typed methods, retries and webhook verification.

The bentocal package on npm wraps every API endpoint in a typed method. It takes the region from your API key, retries rate limits and provider hiccups for you, throws errors with a stable code, and checks webhook signatures in one call.

  • Runs on Node.js 20+, Bun, Deno, Cloudflare Workers and Vercel Edge
  • No dependencies: it uses fetch and Web Crypto
  • Types generated from the API's OpenAPI document, exported for your own code
  • ESM and CommonJS builds
Verify before publishing: the package isn't on npm yet. Remove this callout once npm install bentocal works.
Your API key is a secret, so use the SDK on your server only. It refuses to run in a browser page; the browser only ever needs a connect session's url.

Install

Bash
npm install bentocal

Create a client

The client reads BENTOCAL_API_KEY from the environment, or takes the key as its first argument. The key already says which region your application lives in, so there's no base URL to set. See Authentication and API keys.

TypeScript
import { BentoCal } from 'bentocal';

const bentocal = new BentoCal(); // reads BENTOCAL_API_KEY
// or: new BentoCal(process.env.MY_BENTOCAL_KEY)

With CommonJS:

JavaScript
const { BentoCal } = require('bentocal');

Fields use the API's own names (user_id, slot_duration_minutes), so the API reference applies as written. Responses are plain objects with the same keys as the API's JSON. Every request and response type is exported:

TypeScript
import type { ConnectSession, AvailabilityRequest } from 'bentocal';

Connect a calendar

Create a connect session for one of your users and send them to its url:

TypeScript
const session = await bentocal.connectSessions.create({
  user_id: 'user_123', // your own ID for the user
  redirect_uri: 'https://app.example.com/calendar/connected',
});
// Redirect your user to session.url

They sign in to Google or Microsoft and come back to your redirect_uri with connect_session_id and status in the query string. Read the session to get the connection:

TypeScript
// GET /calendar/connected?connect_session_id=cs_…&status=success
const session = await bentocal.connectSessions.get(connectSessionId);
if (session.status === 'completed') {
  console.log('Connected', session.connection!.email);
}

To relink a connection whose token stopped working, pass its connection_id when you create the session. See Connecting calendars.

Find a time

start and end take a Date or an ISO 8601 string.

TypeScript
const { slots } = await bentocal.availability.query({
  participants: [{ user_id: 'user_123' }, { user_id: 'user_456' }],
  periods: [{ start: new Date('2026-11-02T09:00:00+11:00'), end: new Date('2026-11-02T17:00:00+11:00') }],
  slot_duration_minutes: 30,
});
// [{ start: '2026-11-01T22:00:00Z', end: '2026-11-01T22:30:00Z', user_ids: ['user_123', 'user_456'] }, …]

For raw busy times instead of slots:

TypeScript
const busy = await bentocal.freeBusy.query({
  user_ids: ['user_123'],
  start: new Date('2026-11-02T00:00:00+11:00'),
  end: new Date('2026-11-03T00:00:00+11:00'),
});

See Free/busy and availability.

Book it

Events are keyed by your own ID, so the same call creates the event and later updates it, and a retry never makes a duplicate:

TypeScript
await bentocal.events.upsert('user_123', 'booking_789', {
  summary: 'Intro call',
  start: slots![0]!.start,
  end: slots![0]!.end,
  time_zone: 'Australia/Sydney',
});

await bentocal.events.delete('user_123', 'booking_789');

See Writing events for every field.

Verify webhooks

Pass the raw request body, before any JSON parsing, plus the request headers. The client reads the signing secret from BENTOCAL_WEBHOOK_SECRET, or you can pass secret. verify throws WebhookVerificationError if the signature is missing, wrong or more than five minutes old. During a secret rotation both the old and the new secret are accepted.

TypeScript
// Next.js route handler, Hono, Workers, Bun.serve: anything with a fetch Request
export async function POST(req: Request) {
  const event = await bentocal.webhooks.verify({ payload: await req.text(), headers: req.headers });

  switch (event.type) {
    case 'connection.relink_required':
      await askUserToReconnect(event.data.user_id, event.data.connection_id);
      break;
    case 'calendar.changed':
      await resync(event.data.user_id, event.data.calendar_id);
      break;
  }
  return new Response(null, { status: 204 });
}

With Express, use express.raw so the body isn't parsed first:

TypeScript
app.post('/webhooks/bentocal', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = await bentocal.webhooks.verify({ payload: req.body, headers: req.headers });
  // …
  res.sendStatus(204);
});

Deliveries are retried, so use event.id to skip ones you've already handled. See Webhooks for the event types.

To test a handler locally, sign your own payload and send it in the BentoCal-Signature header:

TypeScript
import { signWebhook } from 'bentocal';

const body = JSON.stringify({ id: 'evt_test', type: 'calendar.changed', created_at: new Date().toISOString(), application_id: 'app_test', data: { /* … */ } });
const signature = await signWebhook({ payload: body, secret: 'whsec_test' });
await fetch('http://localhost:3000/webhooks/bentocal', { method: 'POST', body, headers: { 'BentoCal-Signature': signature } });

Errors

API errors throw BentoCalApiError. Branch on its code, which is stable; message is for people.

TypeScript
import { BentoCalApiError } from 'bentocal';

try {
  await bentocal.events.upsert(userId, bookingId, event);
} catch (err) {
  if (err instanceof BentoCalApiError && err.code === 'relink_required') {
    // Send the user through a new connect session with connection_id
  }
  throw err;
}

BentoCalApiError has status, code, message, requestId (the same ID as in the console's logs; quote it to support), details (validation issues) and retryAfter. Network failures and timeouts throw BentoCalConnectionError. Both extend BentoCalError. See Errors for every code.

Retries and timeouts

Network errors, 429 and 502–504 are retried twice with exponential backoff. When the API sends Retry-After, the SDK waits that long, up to 60 seconds; a longer wait throws instead. Each attempt times out after 30 seconds. See Limits and rate limits.

Set them for the client, or for one call as the method's last argument, which also takes an AbortSignal:

TypeScript
const bentocal = new BentoCal({ maxRetries: 4, timeout: 10_000 }); // milliseconds

await bentocal.users.get('user_123', { timeout: 5_000, signal: AbortSignal.timeout(8_000) });

Configuration

OptionEnvironment variableDefault
apiKeyBENTOCAL_API_KEYRequired
webhookSecretBENTOCAL_WEBHOOK_SECRETNone
baseUrlBENTOCAL_BASE_URLhttps://api.bentocal.com/{region}/v1, region from the key
timeout30000 ms
maxRetries2
fetchglobalThis.fetch

With the environment variables set, new BentoCal() needs no arguments.

All methods

MethodEndpoint
connectSessions.create(body)POST /connect_sessions
connectSessions.get(id)GET /connect_sessions/{id}
users.get(userId)GET /users/{user_id}
users.update(userId, { default_connection_id })PATCH /users/{user_id}
users.delete(userId)DELETE /users/{user_id}
connections.get(id)GET /connections/{id}
connections.delete(id)DELETE /connections/{id}
connections.listCalendars(id)GET /connections/{id}/calendars
freeBusy.query(body)POST /free_busy
availability.query(body)POST /availability
events.upsert(userId, eventId, body)PUT /users/{user_id}/events/{event_id}
events.delete(userId, eventId)DELETE /users/{user_id}/events/{event_id}
webhooks.verify({ payload, headers, secret? })Checks a webhook locally

The package is MIT licensed.