Skip to content
← Public packages

@kentcdodds/cal-com

Cal.com helpers for booking pages, event types, slots, and webhooks.

src/openapi-client.js

163 lines · 5.8 KB · JavaScript
/**
 * Scaffolded by Kody from an OpenAPI spec.
 * Spec: https://cal.com/docs/api-reference/v2/openapi.json (Cal.com API v2)
 * Slugs: mecontroller_getme, bookingscontroller_2026_05_01_getbookings,
 *   bookingscontroller_2026_02_25_getbooking|createbooking|cancelbooking|reschedulebooking,
 *   eventtypescontroller_2024_06_14_geteventtypes|geteventtypebyid,
 *   slotscontroller_2024_09_04_getavailableslots,
 *   webhookscontroller_getwebhooks|createwebhook
 * Auth: bearerSecret cal-com-api-key (unscoped placeholder; verified HTTP 200)
 *
 * Host approval is enforced by Kody's fetch gateway and never widened by this spec.
 */

const API_BASE_URL = 'https://api.cal.com'
const AUTH_HEADER = 'Bearer {{secret:cal-com-api-key}}'

export function buildUrl(pathTemplate, params = {}) {
  return API_BASE_URL + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name) => {
    const value = params[name]
    if (value === undefined || value === null) {
      throw new Error(`Missing required path parameter: ${name}`)
    }
    return encodeURIComponent(String(value))
  })
}

export function appendQuery(url, query = {}) {
  const search = new URLSearchParams()
  for (const [key, value] of Object.entries(query)) {
    if (value === undefined || value === null || value === '') continue
    if (Array.isArray(value)) {
      for (const item of value) {
        if (item === undefined || item === null || item === '') continue
        search.append(key, String(item))
      }
      continue
    }
    search.append(key, String(value))
  }
  const qs = search.toString()
  return qs ? `${url}?${qs}` : url
}

function mergeHeaders(userHeaders, authHeaders) {
  const merged = { ...(userHeaders ?? {}) }
  for (const [key, value] of Object.entries(authHeaders)) {
    const lower = key.toLowerCase()
    for (const existing of Object.keys(merged)) {
      if (existing.toLowerCase() === lower) delete merged[existing]
    }
    merged[key] = value
  }
  return merged
}

function hasHeader(headers, name) {
  const lower = name.toLowerCase()
  return Object.keys(headers).some((key) => key.toLowerCase() === lower)
}

function authHeaders() {
  return { Authorization: AUTH_HEADER }
}

async function call(method, pathTemplate, input = {}, requiredParams = []) {
  const params = input.params ?? {}
  for (const name of requiredParams) {
    if (params[name] === undefined || params[name] === null) {
      throw new Error(`Missing required path parameter: ${name}`)
    }
  }
  const url = appendQuery(buildUrl(pathTemplate, params), input.query)
  const headers = mergeHeaders(input.headers, authHeaders())
  let body
  if (input.body !== undefined) {
    body = JSON.stringify(input.body)
    if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
  }
  return fetch(url, { method, headers, body })
}

/** GET /v2/me — Get my profile */
export async function mecontroller_getme(input = {}) {
  return call('GET', '/v2/me', input)
}

/** GET /v2/bookings — Get all bookings */
export async function bookingscontroller_2026_05_01_getbookings(input = {}) {
  return call('GET', '/v2/bookings', input)
}

/** GET /v2/bookings/{bookingUid} — Get a booking */
export async function bookingscontroller_2026_02_25_getbooking(input = {}) {
  return call('GET', '/v2/bookings/{bookingUid}', input, ['bookingUid'])
}

/** POST /v2/bookings — Create a booking */
export async function bookingscontroller_2026_02_25_createbooking(input = {}) {
  return call('POST', '/v2/bookings', input)
}

/** POST /v2/bookings/{bookingUid}/cancel — Cancel a booking */
export async function bookingscontroller_2026_02_25_cancelbooking(input = {}) {
  return call('POST', '/v2/bookings/{bookingUid}/cancel', input, ['bookingUid'])
}

/** POST /v2/bookings/{bookingUid}/reschedule — Reschedule a booking */
export async function bookingscontroller_2026_02_25_reschedulebooking(input = {}) {
  return call('POST', '/v2/bookings/{bookingUid}/reschedule', input, ['bookingUid'])
}

/** GET /v2/event-types — Get all event types */
export async function eventtypescontroller_2024_06_14_geteventtypes(input = {}) {
  return call('GET', '/v2/event-types', input)
}

/** GET /v2/event-types/{eventTypeId} — Get an event type */
export async function eventtypescontroller_2024_06_14_geteventtypebyid(input = {}) {
  return call('GET', '/v2/event-types/{eventTypeId}', input, ['eventTypeId'])
}

/** GET /v2/slots — Get available time slots for an event type */
export async function slotscontroller_2024_09_04_getavailableslots(input = {}) {
  return call('GET', '/v2/slots', input)
}

/** GET /v2/webhooks — Get all webhooks */
export async function webhookscontroller_getwebhooks(input = {}) {
  return call('GET', '/v2/webhooks', input)
}

/** POST /v2/webhooks — Create a webhook */
export async function webhookscontroller_createwebhook(input = {}) {
  return call('POST', '/v2/webhooks', input)
}

/**
 * Escape-hatch fetch for Cal.com API v2 paths not covered by the scaffold.
 * `path` is relative to https://api.cal.com (include `/v2/...`).
 */
export async function rawCalRequest(path, options = {}) {
  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.')
  }
  const normalized = path.startsWith('/') ? path : `/${path}`
  const url = appendQuery(`${API_BASE_URL}${normalized}`, options.query)
  const headers = mergeHeaders(
    {
      Accept: 'application/json',
      ...(options.headers ?? {}),
    },
    authHeaders(),
  )
  let body = options.body
  if (body !== undefined && typeof body !== 'string') {
    body = JSON.stringify(body)
    if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
  }
  return fetch(url, { method: options.method ?? (body === undefined ? 'GET' : 'POST'), headers, body })
}