Menu

Docs

What you can connect to today.

Scandum · 2 September 2026

Three things: a script tag that puts the web chat door on your site, the scheduling API a voice agent reads a diary and books through, and the webhook every lead is posted to. Each is documented from the code that serves it.

Widget embed

How do I put the web chat door on my site?

One script tag before the closing body tag. The workspace key is in the evening digest email your workspace sends; paste it into data-workspace. The widget writes to the same customer record the phone door writes to, in the visitor's language.

index.html
<script src="https://scandum.com/widget.js" data-workspace="YOUR_WORKSPACE_KEY"></script>

The web chat door is new and in the lab; the tag shape above is the one that ships with it. Your workspace key is a secret to your business, not to the visitor: it identifies the workspace, and the widget can only write, never read.

Speed-to-Lead

Is there an API to push a lead so the qualifier calls it?

Not yet as a public endpoint. POST /api/v1/leads is not implemented. Today a lead reaches the Lead Qualifier through the suite's own forms and landing pages, and reaches you through the webhook below. What is public and stable is the scheduling API: the diary a voice agent reads and books into.

When a leads endpoint ships it will be documented here first, with the same honesty: shape, headers, failure codes. Until then, do not build against a URL this page does not print.

Scheduling API · /api/v1

How does a voice agent read a real diary and book into it?

Four endpoints under /api/v1, one key per calendar. Read availability before offering any time, book with date, time, name and phone, read or cancel by the spoken reference. Every response carries ok; every failure carries a machine-readable error and a message written to be said aloud.

Authentication

Send the calendar's key as a bearer token. The key is the calendar; there is no calendar id in any path. A platform whose tool builder only offers plain headers can send x-api-key instead. With no calendars configured every route answers 503 not_configured; a wrong key answers 401 unauthorized.

headers
Authorization: Bearer <apiKey>
# or
x-api-key: <apiKey>

All dates are YYYY-MM-DD and times HH:mm on a 24-hour clock in the calendar's own timezone; any field named …At is absolute UTC ISO. Writes are limited to 120 in five minutes per key (429 rate_limited).

GET /api/v1/calendar

Who am I talking to and when are they open. Call it on deployment, not on every call: hours, timezone, slot length, notice and horizon, plus the same facts as spoken sentences so none of it is hard-coded into a prompt.

curl -H "Authorization: Bearer $KEY" https://scandum.com/api/v1/calendar

{ "ok": true,
  "calendar": { "id": "…", "name": "…", "timezone": "Asia/Kolkata", "slotMinutes": 30, … },
  "now": { "date": "2026-09-02", "time": "10:14", "weekday": "Wednesday", "timezone": "Asia/Kolkata" },
  "spoken": { "today": "…", "hours": ["Monday 9:30 am to 1 pm and 2 pm to 7 pm", …],
              "notice": "Appointments need at least 30 minutes' notice.",
              "horizon": "The diary is open for the next 30 days." } }

GET /api/v1/availability

Free slots. The call to make before offering anybody a time, and the only times an agent may offer are the ones in days[].slots.

?days=3

The next N days from today, open or closed. Whole number.

?from=&to=

An explicit window.

?date=

One day.

?duration=60

Minutes, for an appointment longer than the default slot. Also accepted as durationMinutes.

curl -H "Authorization: Bearer $KEY" "https://scandum.com/api/v1/availability?days=3"

{ "ok": true,
  "calendar": { "id": "…", "name": "…", "timezone": "Asia/Kolkata", "slotMinutes": 30 },
  "from": "2026-09-02", "to": "2026-09-04", "durationMinutes": 30,
  "note": "Offer only the times in `days[].slots`. …",
  "totalOpen": 30,
  "days": [ { "date": "2026-09-02", "weekday": "Wednesday", "closed": false,
              "slots": [ { "date": "2026-09-02", "time": "11:00",
                           "startsAt": "2026-09-02T05:30:00.000Z", "endsAt": "2026-09-02T06:00:00.000Z",
                           "remaining": 1 } ] } ],
  "soonest": [ { "date": "2026-09-02", "time": "11:00", "startsAt": "…", "spoken": "Wednesday 2 September at 11 am" } ] }

Asks beyond the horizon are clamped, not refused. closedReason on a closed day is weekly or holiday, so the agent can say “we are shut Sundays” rather than “no availability”. Up to six of the soonest slots come pre-phrased in soonest[].spoken.

POST /api/v1/appointments

Book one. Required: date, time, name, phone. Optional: email, reason, notes, durationMinutes (a multiple of the slot length), source. The phone is stored as given and matched on its last ten digits.

curl -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  https://scandum.com/api/v1/appointments \
  -d '{"date":"2026-09-03","time":"11:00","name":"Priya Sharma","phone":"+91 98765 43210","reason":"follow-up"}'

201
{ "ok": true,
  "appointment": { "id": "apt_…", "reference": "QYK9PP", "date": "2026-09-03", "time": "11:00",
                   "durationMinutes": 30, "startsAt": "2026-09-03T05:30:00.000Z", "status": "confirmed", … },
  "spoken": "Booked for Thursday 3 September at 11 am. The reference is Q Y K 9 P P." }

The reference is drawn from an alphabet with no O, 0, I, 1, S or 5, because it is read down a phone line and written by someone holding a pen.

errorstatusmeans
slot_taken409Gone since the availability call. Carries alternatives, the nearest openings; offer one.
outside_hours409Not a slot that day, for example inside the lunch gap.
closed409Weekly closure or a holiday.
too_soon409Inside the notice window.
beyond_horizon400Further ahead than the diary opens; the message names the last bookable date.
bad_duration400Not a whole number, or not a multiple of the slot length.
bad_time400Time not in HH:mm.
invalid_name / invalid_phone400Ask the caller again.
bad_json400The body was not a JSON object.
unauthorized401Key missing or wrong.
not_found404No appointment with that reference.
rate_limited429More than 120 writes in five minutes on one key.
not_configured503This deployment has no calendars set up.

GET /api/v1/appointments

Find appointments. Filters: phone (matched on the last ten digits), date, from and to, status (confirmed by default, or cancelled or all), limit (up to three digits). Each appointment comes with a spoken line.

curl -H "Authorization: Bearer $KEY" "https://scandum.com/api/v1/appointments?phone=9876543210"

{ "ok": true, "count": 1, "appointments": [ { …, "spoken": "Thursday 3 September at 11 am" } ] }

GET, PATCH, DELETE /api/v1/appointments/[ref]

[ref] is either the spoken reference (case and hyphens ignored) or the internal apt_… id. GET reads one back. PATCH with date and time moves it and keeps the reference; PATCH with {"status":"cancelled"} cancels it, for tool builders that only emit PATCH. DELETE cancels it, with an optional ?reason=. Cancelling twice is not an error: an agent retrying after a dropped connection must not hear a failure the second time.

curl -H "Authorization: Bearer $KEY" https://scandum.com/api/v1/appointments/QYK9PP

curl -X PATCH -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  https://scandum.com/api/v1/appointments/QYK9PP -d '{"date":"2026-09-04","time":"16:00"}'
# → { "ok": true, "appointment": { … }, "spoken": "Moved to Friday 4 September at 4 pm. The reference is the same." }

curl -X DELETE -H "Authorization: Bearer $KEY" \
  "https://scandum.com/api/v1/appointments/QYK9PP?reason=caller+changed+their+mind"
# → { "ok": true, "appointment": { …, "status": "cancelled" }, "spoken": "That's cancelled." }

Not the demo desk

The board on the home page runs on /api/bookings/*, a shared sandbox that shows one day and forgets it a few hours later; its write route takes an x-agent-key header. It shares no storage and no rules with /api/v1. Never point a client's agent at it.

Webhooks

Where does a lead go the moment it arrives?

Every form submission is written to the store and, when LEAD_WEBHOOK_URL is set, posted to that URL as JSON with a four-second timeout. The body carries a ready-made text line, which Slack renders as is, and the full lead object, which Zapier and n8n keep field by field.

POST <LEAD_WEBHOOK_URL> · content-type: application/json
{
  "text": "New lead — quote\nPriya Sharma · Sharma Dental — priya@example.com / +919876543210\nvia quotes-form\n“Twenty quotes from August.”",
  "lead": {
    "id": "ld_3f9a1c2b",
    "at": "2026-09-02T04:44:10.000Z",
    "kind": "contact",                 // or "demo-call"
    "name": "Priya Sharma",
    "email": "priya@example.com",      // optional
    "phone": "+919876543210",          // optional, +91 and ten digits
    "company": "Sharma Dental",        // optional
    "message": "Twenty quotes from August.",   // optional
    "intent": "quote",                 // discovery-call | demo-waitlist | product-waitlist | careers | quote
    "industry": "…",                   // demo-call only
    "source": "quotes-form",           // the page or component that sent it
    "ip": "…"
  }
}
Retries

None. The webhook is tried once, in parallel with the store, with a four-second timeout.

Success

Any 2xx. A non-2xx is logged as rejected and counts as not delivered.

Not configured

Not a failure: the store is still a sink. When neither the store nor the webhook takes a lead, the contact API answers 502 not_delivered so the visitor is told to email instead of thanked for a message nobody received.

Logging

Phone numbers and emails are masked in logs; a full number is never written.

Outbound on booking

There is no webhook on appointment writes yet. The scheduling API holds the diary; a webhook-out on write is the natural next step and does not exist today.

Questions about any of this: contact@scandum.com. The AI disclosure every call opens with is on the legal page.

Call the demo number