Developer documentation

Every SchedulerCX account is an API surface: a versioned REST API described by an OpenAPI 3.1 contract, HMAC-signed webhooks, and a built-in MCP server so AI agents can check availability, book, reschedule and cancel on your behalf — all against the same rules as your public booking page.

Overview

Everything is scoped to one tenant. A credential (API key or OAuth client) belongs to exactly one account, and every resource it can see or touch belongs to that account — there is no cross-tenant access, so you never pass a user id.

Three integration surfaces share one service layer, so they can never disagree about what's bookable:

REST API

Versioned under /api/v1, described by OpenAPI 3.1. Idempotency keys, cursor pagination, RFC 7807 errors.

Webhooks

HMAC-signed POSTs for booking.created, booking.requested, booking.approved, booking.cancelled and booking.rescheduled.

MCP server

Six agent tools over streamable HTTP at /api/mcp, authenticated with the same credentials.

Base URLs

SurfaceURL
REST API basehttp://portal.dialoguecx.ai/api/v1
OpenAPI 3.1 spechttp://portal.dialoguecx.ai/openapi.yaml
MCP server (streamable HTTP)http://portal.dialoguecx.ai/api/mcp
OAuth 2.0 token endpointhttp://portal.dialoguecx.ai/api/v1/oauth/token
Embed scripthttp://portal.dialoguecx.ai/embed.js

Quickstart

Create an API key in Dashboard → API & MCP (it's shown once — store it as $KEY), then:

# 1. Grab your profile — sanity-checks the key
curl -H "Authorization: Bearer $KEY" http://portal.dialoguecx.ai/api/v1/me

# 2. Find open slots for an event type
curl -H "Authorization: Bearer $KEY" \
  "http://portal.dialoguecx.ai/api/v1/availability?event_type=intro-call&start=2026-07-10&end=2026-07-14"

# 3. Book one of them (idempotently)
curl -X POST http://portal.dialoguecx.ai/api/v1/bookings \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "event_type": "intro-call",
    "start_time": "2026-07-10T10:00:00-04:00",
    "invitee_name": "Ada Lovelace",
    "invitee_email": "ada@example.com"
  }'

Authentication

Both the REST API and the MCP server accept the same two bearer credentials (RFC 6750): long-lived API keys, or short-lived OAuth 2.0 access tokens. Send either as Authorization: Bearer <token>.

API keys

Keys look like sk_live_… (or sk_test_…), are scoped at creation, revocable, and stored server-side only as a peppered HMAC-SHA256 hash — the full key is shown exactly once. Best for server-to-server integrations and MCP clients you control.

OAuth 2.0 client credentials

For platforms that prefer short-lived tokens, exchange a client id/secret for a one-hour access token (RFC 6749 §4.4). The optional scope parameter requests a subset of the client's granted scopes — tokens never exceed what the credential was granted, even if grants are later reduced.

curl -X POST http://portal.dialoguecx.ai/api/v1/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=$CLIENT_ID \
  -d client_secret=$CLIENT_SECRET \
  -d "scope=availability:read bookings:read bookings:write"

# → { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600,
#     "scope": "availability:read bookings:read bookings:write" }

Scopes

Every operation requires a scope; a missing scope yields 403 with type …/errors/forbidden listing required_scope and granted_scopes.

ScopeGrants
profile:readRead the tenant profile (GET /me)
profile:writeUpdate profile settings (PATCH /me)
event-types:readList and read event types
event-types:writeCreate, update, delete event types
availability:readCompute bookable slots
bookings:readList, read and search bookings
bookings:writeCreate, reschedule, cancel bookings
webhooks:manageCreate, list, delete webhook endpoints

Conventions

Timestamps & timezones

All instants are RFC 3339 / ISO 8601 with an explicit UTC offset, e.g. 2026-07-10T14:00:00-04:00, rendered in the resource's IANA timezone. Storage is UTC; DST math is done per calendar date in the tenant's zone, so a "9:00–17:00" rule stays 9:00–17:00 wall-clock across transitions. Where a bare date is accepted (availability start/end), it is interpreted in the request's timezone, inclusive on the end date.

Errors (RFC 7807)

Every error is an application/problem+json document with a stable type URI you can branch on — never match on human-readable text. Conflict responses carry extensions that make retries possible without a human:

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "http://portal.dialoguecx.ai/errors/slot-conflict",
  "title": "Requested time is not available",
  "status": 409,
  "detail": "That slot was booked a moment ago.",
  "instance": "/api/v1/bookings",
  "conflicting_bookings": [
    { "id": "bk_123", "start_time": "2026-07-10T10:00:00-04:00",
      "end_time": "2026-07-10T10:30:00-04:00" }
  ],
  "alternative_slots": [
    { "start": "2026-07-10T10:30:00-04:00", "end": "2026-07-10T11:00:00-04:00" },
    { "start": "2026-07-10T11:00:00-04:00", "end": "2026-07-10T11:30:00-04:00" }
  ]
}
type slugStatusMeaning
validation400Malformed or missing request fields
unauthorized401Missing, invalid, or revoked credential
forbidden403Credential lacks the required scope
not-found404No such resource in this tenant
slot-conflict409Slot taken / outside availability — includes alternative_slots
ambiguous-match409Lookup matched several bookings — includes matches to retry with
idempotency-conflict409Idempotency-Key reused with a different body
already-cancelled409Booking is already cancelled
has-upcoming-bookings409Event type still has confirmed future bookings
slug-taken409Event type slug already in use
missing-answers422Required custom questions unanswered
rate-limited429Per-credential limit hit — honor Retry-After
internal500Unexpected server error

Idempotency

Every mutating endpoint accepts an Idempotency-Key header (any unique string ≤ 255 chars; UUIDv4 recommended). Retrying with the same key and body replays the original response (marked Idempotency-Replayed: true); the same key with a different body returns 409 idempotency-conflict. Always send one when creating bookings from a queue or an agent — a timeout plus a retry must not double-book your invitee.

Rate limits

Limits are enforced per credential. On 429 you get a Retry-After header (seconds) and a rate-limited problem document — back off and retry after that interval.

REST API

The full request/response contract lives in the interactive API reference (Swagger UI) — you can authorize with an API key there and call this deployment directly. The table below is the map:

Endpoint reference

MethodPathScopeWhat it does
GET/meprofile:readTenant profile, booking page & MCP URLs
PATCH/meprofile:writeUpdate name, handle, timezone, notification email
GET/event-typesevent-types:readList event types (filter: active)
POST/event-typesevent-types:writeCreate an event type
GET/event-types/{id}event-types:readGet one event type
PATCH/event-types/{id}event-types:writePartial update
DELETE/event-types/{id}event-types:writeDelete (409 if upcoming bookings exist)
GET/availabilityavailability:readBookable slots for an event type (≤ 60 days)
GET/bookingsbookings:readList/search bookings (status, invitee, range)
POST/bookingsbookings:writeCreate a booking at an available slot
GET/bookings/{id}bookings:readGet one booking
DELETE/bookings/{id}bookings:writeCancel (soft; optional ?reason=)
POST/bookings/{id}/approvebookings:writeConfirm a pending booking request
POST/bookings/{id}/reschedulebookings:writeMove to a new start_time
GET/webhookswebhooks:manageList webhook endpoints
POST/webhookswebhooks:manageCreate endpoint (secret shown once)
DELETE/webhooks/{id}webhooks:manageDelete endpoint
POST/oauth/tokennoneExchange client credentials for a token

All paths are relative to http://portal.dialoguecx.ai/api/v1.

Reading availability

GET /availability returns real bookable slots: weekly rules and date overrides, minus existing bookings, buffers, and the event type's minimum notice. Never compute slots yourself — a slot the endpoint returns is one POST /bookings will accept (barring a race, which is handled below).

curl -H "Authorization: Bearer $KEY" \
  "http://portal.dialoguecx.ai/api/v1/availability?event_type=intro-call&start=2026-07-10&end=2026-07-12&timezone=America/Toronto"

{
  "event_type_id": "et_8f7d…",
  "timezone": "America/Toronto",
  "slots": [
    { "start": "2026-07-10T09:00:00-04:00", "end": "2026-07-10T09:30:00-04:00" },
    { "start": "2026-07-10T09:30:00-04:00", "end": "2026-07-10T10:00:00-04:00" },
    …
  ]
}

Booking lifecycle

Bookings move through three states: confirmed cancelled (soft, keeps the record) or rescheduled (the original points to its replacement via rescheduled_from_id). Double-booking is impossible even under concurrent writers — a database exclusion constraint guarantees it — so a lost race surfaces as a clean 409 slot-conflict with alternative_slots you can offer instead.

# Reschedule: old booking → status "rescheduled", replacement returned
curl -X POST http://portal.dialoguecx.ai/api/v1/bookings/bk_123/reschedule \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "start_time": "2026-07-11T14:00:00-04:00" }'

# Cancel: soft state change, record stays readable
curl -X DELETE "http://portal.dialoguecx.ai/api/v1/bookings/bk_123?reason=Client%20asked" \
  -H "Authorization: Bearer $KEY"

Webhooks

Register an endpoint with POST /webhooks choosing any of booking.created, booking.requested, booking.approved, booking.cancelled, booking.rescheduled. The signing secret is returned only on creation. Deliveries time out after 5 s; respond 2xx quickly and process async.

What a delivery looks like
POST <your endpoint>
Content-Type: application/json
X-Timestamp: 1783958400
X-Signature: sha256=7f3b…c9d1
User-Agent: SchedulerCX-Webhooks/1.0

{
  "id": "evt_lyx8k2ab3f",
  "event": "booking.created",
  "created_at": "2026-07-10T14:00:02.000Z",
  "data": { …the booking, same shape as GET /bookings/{id}… }
}

Verify like Stripe/GitHub: recompute the HMAC over `${X-Timestamp}.${rawBody}` and compare in constant time, rejecting stale timestamps:

Node / Express verification
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/schedulercx",
  express.raw({ type: "application/json" }), // keep the RAW body — sign bytes, not JSON
  (req, res) => {
    const ts = req.header("X-Timestamp") ?? "";
    const sig = req.header("X-Signature") ?? "";

    // 1. Reject stale timestamps (replay protection)
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);

    // 2. Recompute HMAC over "<timestamp>.<raw body>" and compare constant-time
    const expected =
      "sha256=" +
      createHmac("sha256", process.env.SCHEDULERCX_WEBHOOK_SECRET)
        .update(`${ts}.${req.body}`)
        .digest("hex");
    const a = Buffer.from(sig), b = Buffer.from(expected);
    if (a.length !== b.length || !timingSafeEqual(a, b)) return res.sendStatus(401);

    const { event, data } = JSON.parse(req.body.toString("utf8"));
    // event: "booking.created" | "booking.requested" | "booking.approved"
    //      | "booking.cancelled" | "booking.rescheduled"

    res.sendStatus(200);
  },
);

MCP server

The Model Context Protocol server at http://portal.dialoguecx.ai/api/mcp (streamable HTTP transport) exposes scheduling as agent tools. It authenticates with the same bearer credentials as the REST API and is implemented on the same service layer — an agent booking a slot obeys exactly the same availability, buffer, notice and conflict rules, and bookings it creates are marked created_via: "agent".

Tools

ToolScopeWhat it does
list_event_typesevent-types:readBookable meeting kinds, incl. required custom questions
get_availabilityavailability:readOpen slots for an event type in a date range (≤ 60 days)
create_bookingbookings:writeBook a slot; conflicts return alternative_slots to offer
find_bookingsbookings:readSearch by status, event type, invitee email/phone, time range
cancel_bookingbookings:writeCancel by booking_id or invitee details (+ date to disambiguate)
reschedule_bookingbookings:writeMove a booking to a new available slot

Tool errors return the same RFC 7807 documents as REST, as structured tool results — so an agent hitting a slot conflict sees alternative_slots and can offer the invitee a new time, and an ambiguous cancel request gets a matches list to ask the human about. Give agent credentials only the scopes they need — a read-only assistant works fine with just event-types:read + availability:read + bookings:read.

Connect from Claude Code

claude mcp add --transport http schedulercx http://portal.dialoguecx.ai/api/mcp \
  --header "Authorization: Bearer sk_live_your_key_here"

# then, inside Claude Code:
#   > find a 30-minute slot next Tuesday afternoon and book it
#   > for jane@acme.com — she wants to discuss the Q3 rollout

Connect from Claude Desktop / claude.ai

Add a custom connector (Settings → Connectors → Add custom connector) pointing at http://portal.dialoguecx.ai/api/mcp, or use an mcp.json-style config where your client supports HTTP servers with headers:

.mcp.json / mcp.json
{
  "mcpServers": {
    "schedulercx": {
      "type": "http",
      "url": "http://portal.dialoguecx.ai/api/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_your_key_here"
      }
    }
  }
}

Other clients (stdio-only)

Clients that only speak stdio can bridge through mcp-remote:

claude_desktop_config.json (or equivalent)
{
  "mcpServers": {
    "schedulercx": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://portal.dialoguecx.ai/api/mcp",
        "--header", "Authorization: Bearer sk_live_your_key_here"
      ]
    }
  }
}

Your personal MCP URL also appears in GET /me as mcp_url, and in Dashboard → API & MCP.

Embed widget

Drop the booking page into any site with one script tag — inline, or as a popup triggered by any element. Replace your-slug with your booking page slug:

<!-- Inline embed: renders the booking page in place -->
<div data-schedulercx-inline="http://portal.dialoguecx.ai/your-slug" style="min-height:700px"></div>
<script src="http://portal.dialoguecx.ai/embed.js" async></script>

<!-- Popup: opens the booking page in a modal -->
<button data-schedulercx-popup="http://portal.dialoguecx.ai/your-slug">Book a time</button>
<script src="http://portal.dialoguecx.ai/embed.js" async></script>
Questions the docs don't answer? The OpenAPI contract is the source of truth, and the Swagger UI lets you exercise every endpoint against this deployment.