import {
bookingscontroller_2026_02_25_cancelbooking,
bookingscontroller_2026_02_25_createbooking,
bookingscontroller_2026_02_25_getbooking,
bookingscontroller_2026_02_25_reschedulebooking,
bookingscontroller_2026_05_01_getbookings,
eventtypescontroller_2024_06_14_geteventtypebyid,
eventtypescontroller_2024_06_14_geteventtypes,
mecontroller_getme,
rawCalRequest,
slotscontroller_2024_09_04_getavailableslots,
webhookscontroller_createwebhook,
webhookscontroller_getwebhooks,
} from './openapi-client.js'
import { DEFAULT_BOOKING_BASE_URL, setupUrls } from './setup.js'
const DEFAULT_API_VERSION = '2024-08-13'
const BOOKINGS_LIST_API_VERSION = '2026-05-01'
const BOOKING_API_VERSION = '2026-02-25'
const SLOTS_API_VERSION = '2024-09-04'
const EVENT_TYPES_API_VERSION = '2024-06-14'
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function withoutKeys(record, keys) {
const blocked = new Set(keys)
return Object.fromEntries(
Object.entries(record ?? {})
.filter(([, value]) => value !== undefined)
.filter(([key]) => !blocked.has(key)),
)
}
const AUTH_AND_CONTROL_KEYS = [
'action',
'apiVersion',
'body',
'headers',
'params',
'dryRun',
'confirm',
'secretName',
'integrationName',
'account',
'bookingBaseUrl',
]
function queryFromInput(input, ignoredKeys = []) {
if (isRecord(input?.query)) return input.query
return withoutKeys(input, [...AUTH_AND_CONTROL_KEYS, ...ignoredKeys])
}
function bodyFromInput(input, ignoredKeys = []) {
if (isRecord(input?.body)) return input.body
return withoutKeys(input, [...AUTH_AND_CONTROL_KEYS, 'query', ...ignoredKeys])
}
function authFromInput(input) {
return {
secretName: input?.secretName,
integrationName: input?.integrationName,
account: input?.account,
}
}
function bookingUidFromInput(input) {
const value = input?.bookingUid ?? input?.uid ?? input?.params?.bookingUid
if (typeof value !== 'string' || value.trim() === '') {
throw new Error('Cal.com helper requires bookingUid (uid is accepted as an alias).')
}
return value
}
function versionHeaders(apiVersion) {
if (!apiVersion) return {}
return { 'cal-api-version': apiVersion }
}
function requireConfirm(input, action, path, body) {
if (input?.dryRun) {
return { dryRun: true, method: 'POST', path, body }
}
if (input?.confirm !== true) {
throw new Error(
`${action} writes live Cal.com state. Pass dryRun: true to preview, or confirm: true to apply.`,
)
}
return null
}
async function parseBody(response) {
const contentType = response.headers.get('content-type') ?? ''
if (contentType.includes('application/json')) return await response.json()
const text = await response.text()
return text ? { text } : null
}
function unwrapData(body) {
if (isRecord(body) && Object.prototype.hasOwnProperty.call(body, 'data')) return body.data
return body
}
async function parseOk(response, pathHint) {
const data = await parseBody(response)
if (!response.ok) {
const error = new Error(`Cal.com API request failed with ${response.status} ${response.statusText}`)
error.status = response.status
error.statusText = response.statusText
error.path = pathHint
error.data = data
throw error
}
return data
}
async function callParsed(fn, input, pathHint) {
return unwrapData(await parseOk(await fn(input), pathHint))
}
/**
* Escape-hatch Cal.com API v2 request. Prefer named helpers when available.
* Path is relative to https://api.cal.com (include `/v2/...`) or legacy `/me`-style
* paths which are rewritten to `/v2/...`.
*/
export async function calRequest(input = {}) {
if (!isRecord(input)) throw new Error('Cal.com request input must be an object.')
let path = input.path
if (typeof path !== 'string' || path.trim() === '') {
throw new Error('Cal.com request requires a non-empty path string.')
}
if (/^https?:\/\//i.test(path)) {
throw new Error('Cal.com request path must be relative, for example /v2/me or /me.')
}
path = path.startsWith('/') ? path : `/${path}`
if (!path.startsWith('/v2/') && path !== '/v2') {
path = `/v2${path}`
}
const method = String(input.method ?? (input.body === undefined ? 'GET' : 'POST')).toUpperCase()
const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(method)
if (mutating) {
const preview = requireConfirm(input, 'calRequest', path, input.body)
if (preview) return { ...preview, method, query: input.query ?? null }
} else if (input.dryRun) {
return { dryRun: true, method, path, query: input.query ?? null, body: input.body ?? null }
}
const headers = {
...versionHeaders(input.apiVersion ?? DEFAULT_API_VERSION),
...(input.headers ?? {}),
}
const response = await rawCalRequest(path, {
method,
query: input.query,
headers,
body: input.body,
...authFromInput(input),
})
const data = await parseOk(response, path)
return { status: response.status, data }
}
export async function getProfile(input = {}) {
return callParsed(
() =>
mecontroller_getme({
...authFromInput(input),
headers: {
...versionHeaders(input.apiVersion),
...(input.headers ?? {}),
},
}),
input,
'/v2/me',
)
}
function trimProfile(profile) {
return {
id: profile?.id,
username: profile?.username,
email: profile?.email,
name: profile?.name,
timeZone: profile?.timeZone,
defaultScheduleId: profile?.defaultScheduleId,
}
}
function isAuthSetupFailure(error) {
const status = error?.status
if (status === 401 || status === 403) return true
const message = String(error?.message ?? error)
return /secret|host approval|not connected|not approved|placeholder|calComApiKey|unresolved/i.test(
message,
)
}
/**
* Verify Cal.com credentials with a trimmed `/me` read.
* Without credentials, returns setup URLs instead of throwing.
*/
export async function smokeTest(input = {}) {
const setup = setupUrls()
if (input?.dryRun) {
return { ok: true, live: false, dryRun: true, method: 'GET', path: '/v2/me', setup }
}
try {
const profile = await getProfile(input)
return { ok: true, live: true, profile: trimProfile(profile), setup }
} catch (error) {
if (isAuthSetupFailure(error)) {
return {
ok: true,
live: false,
setup,
reason: error instanceof Error ? error.message : String(error),
status: error?.status ?? null,
}
}
throw error
}
}
export async function listBookings(input = {}) {
return callParsed(
() =>
bookingscontroller_2026_05_01_getbookings({
...authFromInput(input),
query: queryFromInput(input),
headers: {
...versionHeaders(input.apiVersion ?? BOOKINGS_LIST_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
'/v2/bookings',
)
}
export async function getBooking(input = {}) {
const bookingUid = bookingUidFromInput(input)
return callParsed(
() =>
bookingscontroller_2026_02_25_getbooking({
...authFromInput(input),
params: { bookingUid },
headers: {
...versionHeaders(input.apiVersion ?? BOOKING_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
`/v2/bookings/${bookingUid}`,
)
}
export async function createBooking(input = {}) {
const body = bodyFromInput(input)
if (!body.eventTypeId) {
throw new Error('createBooking requires eventTypeId.')
}
if (!body.start) {
throw new Error('createBooking requires start (UTC ISO timestamp).')
}
if (!isRecord(body.attendee) || !body.attendee.email || !body.attendee.name) {
throw new Error('createBooking requires attendee.name and attendee.email.')
}
const preview = requireConfirm(input, 'createBooking', '/v2/bookings', body)
if (preview) return preview
return callParsed(
() =>
bookingscontroller_2026_02_25_createbooking({
...authFromInput(input),
body,
headers: {
...versionHeaders(input.apiVersion ?? BOOKING_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
'/v2/bookings',
)
}
export async function cancelBooking(input = {}) {
const bookingUid = bookingUidFromInput(input)
const body = bodyFromInput(input, ['bookingUid', 'uid'])
const path = `/v2/bookings/${bookingUid}/cancel`
const preview = requireConfirm(input, 'cancelBooking', path, body)
if (preview) return preview
return callParsed(
() =>
bookingscontroller_2026_02_25_cancelbooking({
...authFromInput(input),
params: { bookingUid },
body,
headers: {
...versionHeaders(input.apiVersion ?? BOOKING_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
path,
)
}
export async function rescheduleBooking(input = {}) {
const bookingUid = bookingUidFromInput(input)
const body = bodyFromInput(input, ['bookingUid', 'uid'])
if (!body.start) {
throw new Error('rescheduleBooking requires start (UTC ISO timestamp).')
}
const path = `/v2/bookings/${bookingUid}/reschedule`
const preview = requireConfirm(input, 'rescheduleBooking', path, body)
if (preview) return preview
return callParsed(
() =>
bookingscontroller_2026_02_25_reschedulebooking({
...authFromInput(input),
params: { bookingUid },
body,
headers: {
...versionHeaders(input.apiVersion ?? BOOKING_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
path,
)
}
export async function listEventTypes(input = {}) {
return callParsed(
() =>
eventtypescontroller_2024_06_14_geteventtypes({
...authFromInput(input),
query: queryFromInput(input),
headers: {
...versionHeaders(input.apiVersion ?? EVENT_TYPES_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
'/v2/event-types',
)
}
export async function getEventType(input = {}) {
const id = input?.id ?? input?.eventTypeId ?? input?.params?.eventTypeId
if (id === undefined || id === null || id === '') {
throw new Error('Cal.com helper requires id or eventTypeId.')
}
return callParsed(
() =>
eventtypescontroller_2024_06_14_geteventtypebyid({
...authFromInput(input),
params: { eventTypeId: id },
headers: {
...versionHeaders(input.apiVersion ?? EVENT_TYPES_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
`/v2/event-types/${id}`,
)
}
/**
* Available slots for an event type over a date range.
* Returns the unwrapped Cal.com slots payload (date → slot list).
*/
export async function getAvailableSlots(input = {}) {
const query = queryFromInput(input)
if (query.eventTypeId === undefined || query.eventTypeId === null || query.eventTypeId === '') {
throw new Error('getAvailableSlots requires eventTypeId.')
}
if (!query.start || !query.end) {
throw new Error('getAvailableSlots requires start and end dates.')
}
return callParsed(
() =>
slotscontroller_2024_09_04_getavailableslots({
...authFromInput(input),
query,
headers: {
...versionHeaders(input.apiVersion ?? SLOTS_API_VERSION),
...(input.headers ?? {}),
},
}),
input,
'/v2/slots',
)
}
export async function listWebhooks(input = {}) {
return callParsed(
() =>
webhookscontroller_getwebhooks({
...authFromInput(input),
query: queryFromInput(input),
headers: {
...versionHeaders(input.apiVersion),
...(input.headers ?? {}),
},
}),
input,
'/v2/webhooks',
)
}
export async function createWebhook(input = {}) {
const body = bodyFromInput(input)
if (typeof body.subscriberUrl !== 'string' || body.subscriberUrl.trim() === '') {
throw new Error('createWebhook requires subscriberUrl.')
}
const preview = requireConfirm(input, 'createWebhook', '/v2/webhooks', body)
if (preview) return preview
return callParsed(
() =>
webhookscontroller_createwebhook({
...authFromInput(input),
body,
headers: {
...versionHeaders(input.apiVersion),
...(input.headers ?? {}),
},
}),
input,
'/v2/webhooks',
)
}
function bookingUrlForEventType(eventType, username, bookingBaseUrl) {
const explicit = eventType?.link ?? eventType?.bookingUrl
if (typeof explicit === 'string' && /^https?:\/\//i.test(explicit)) return explicit
const slug = eventType?.slug
if (username && slug) return `${bookingBaseUrl}/${username}/${slug}`
return null
}
function eventTypeListFromPayload(eventTypes) {
if (Array.isArray(eventTypes)) return eventTypes
if (Array.isArray(eventTypes?.eventTypeGroups)) {
return eventTypes.eventTypeGroups.flatMap((group) =>
Array.isArray(group?.eventTypes) ? group.eventTypes : [],
)
}
if (Array.isArray(eventTypes?.eventTypes)) return eventTypes.eventTypes
return []
}
/**
* Compact booking-page summary: authenticated profile + event types with booking URLs.
* URLs come from each event type's `link` when present, otherwise
* `{bookingBaseUrl}/{username}/{slug}` using the connected account username.
* No personal booking paths are hard-coded.
*/
export async function summarizeBookingPages(input = {}) {
const [profile, eventTypes] = await Promise.all([getProfile(input), listEventTypes(input)])
const username = profile?.username
const bookingBaseUrl = String(input.bookingBaseUrl ?? DEFAULT_BOOKING_BASE_URL).replace(/\/$/, '')
const pages = eventTypeListFromPayload(eventTypes).map((eventType) => {
const slug = eventType?.slug
return {
id: eventType?.id,
title: eventType?.title ?? eventType?.name,
slug,
lengthInMinutes: eventType?.lengthInMinutes ?? eventType?.length,
hidden: eventType?.hidden,
bookingUrl: bookingUrlForEventType(eventType, username, bookingBaseUrl),
}
})
return {
username,
name: profile?.name,
timeZone: profile?.timeZone,
bookingBaseUrl,
eventTypeCount: pages.length,
pages,
}
}
const actions = {
request: calRequest,
'setup': () => setupUrls(),
'get-profile': getProfile,
profile: getProfile,
'smoke-test': smokeTest,
smoke: smokeTest,
'list-bookings': listBookings,
bookings: listBookings,
'get-booking': getBooking,
'create-booking': createBooking,
'cancel-booking': cancelBooking,
'reschedule-booking': rescheduleBooking,
'list-event-types': listEventTypes,
'event-types': listEventTypes,
'get-event-type': getEventType,
'get-available-slots': getAvailableSlots,
slots: getAvailableSlots,
'list-webhooks': listWebhooks,
webhooks: listWebhooks,
'create-webhook': createWebhook,
'summarize-booking-pages': summarizeBookingPages,
'booking-pages': summarizeBookingPages,
}
/**
* Dispatch Cal.com helper actions such as `list-event-types` or `smoke-test`.
* @param {Object} [input]
* @param {string} [input.action] Action name. Defaults to `smoke-test`.
* @param {boolean} [input.dryRun] Preview mutating calls without writing.
* @returns {Promise<unknown>} Action-specific Cal.com API payload.
* @example
* import calCom from 'kody:@kody/cal-com'
* const result = await calCom({ action: 'list-event-types' })
* // => [{ id: 123, title: '30 Min Meeting', ... }]
*/
export default async function calCom(input = {}) {
const action = input.action ?? 'smoke-test'
const handler = actions[action]
if (!handler) {
throw new Error(`Unsupported Cal.com action: ${action}`)
}
return await handler(input)
}