Reference

Slot booking

Book video-call (vKYC) appointments over /v3 — available time intervals for a date, meeting requests, status, reschedule and cancel — drop-in compatible with the meeting-time / meeting-request slot-booking contract.

The slot-booking endpoints let a partner app book a video-call verification appointment for an applicant: list the free time intervals on a date, save a meeting request for one of them, then read, move or cancel it. Bookings land on the same scheduling engine the hosted flow uses, so agents see them under Video Calls → Upcoming bookings, the applicant is reminded when joining opens, and the call runs in the verification session's Video Call step.

The two core calls keep the widely used slot-booking contract — the same paths, field names, DD/MM/YYYY dates, 24-hour preferredTime, "hh:mm AM/PM" slot times, the flat [{id, time}] list and the {message, status, data: {meetingRequestId}} booking response — so an app that already integrates a backend with this contract switches by changing its base URL and adding the API key header — even the numeric purposeId it already sends keeps working once the workflow is given that code (see Purpose codes).

Info

These endpoints are on the /v3 surface: https://<your-domain>/v3, with the tenant API key in x-api-key. Every path answers with and without a trailing slash.

Endpoints

EndpointPurpose
GET /v3/meeting-purposeThe purposeId values you can book
POST /v3/meeting-time/get-availableFree time intervals on a date → a bare [{id, time}] array
POST /v3/meeting-request/saveBook an interval → 201 {message, status: "CREATED", data}
GET /v3/meeting-request/{meeting_request}Status of a booking
POST /v3/meeting-request/{meeting_request}/rescheduleMove a booking to another interval
POST /v3/meeting-request/{meeting_request}/cancelCancel a booking and free its seat

{meeting_request} is either the booking's booking_id (uuid) or its meetingRequestId (000000324 — zero padding optional).

How the contract maps onto ThirdFactor

Contract fieldIn ThirdFactor
purposeIdA workflow whose Video Call step has Scheduling switched on (Console → Workflows → Video Call). The workflow decides what the call covers; its availability (Console → Video calls → Availability) decides when agents take calls. Sent as the workflow's 9-digit numeric code (000000001, 000000011, …) — or its UUID. List them with GET /v3/meeting-purpose.
timeIntervalIdA bookable slot — generated from the agents' weekly availability windows, or published by hand as a one-off. The id is stable: the one you list is the one you book, and it survives an admin adding more availability. As in the contract, an interval is a time of day — sent with another preferredDate, the same interval on that date is booked.
date, preferredDate, preferredTimeRead in the purpose's schedule timezone (the Availability page's zone, e.g. Asia/Kathmandu), returned as timezone everywhere.
meetingRequestIdA per-tenant sequence, zero-padded to nine digits (000000001, 000000002, …).
email, phone, fullname, title, existingCustomerThe applicant, as you send them. Stored on the booking and shown to agents.

ThirdFactor adds, alongside the contract's keys: booking_id, session_id and session_url (the verification session hosting the call and the link the applicant joins from), booking_status, queue_state (where the call stands for agents), workflow_id, and the slot's starts_at / ends_at as ISO 8601 instants. None of the contract's keys change shape.

Purpose codes

Every workflow of your tenant is numbered once, in creation order, and keeps its number — that 9-digit code is its purposeId. GET /v3/meeting-purpose lists the bookable ones; Console → Video calls → Availability → Booking API purposes shows every workflow's code with a copy button, and an admin with Manage KYC can renumber a workflow — e.g. to 000000011, the purposeId an existing integration already hard-codes — so that integration needs no change. Codes are unique per tenant and never reused.

No session needed — or bring your own

A booking needs no verification session up front. When you send applicant details without session_id, the booking creates the session that will host the call:

  • It runs the purpose workflow, with the applicant's name, email and phone filled in, and stays open until a day after the booked slot.
  • It is charged like POST /v3/session/ — the SESSION_CREATE rate (1 credit unless your platform manager set another). A booking the balance can't cover is refused with 402 and nothing is booked.
  • The applicant gets a confirmation with the verification link — by email (with an add-to-calendar link) when you sent one, else by SMS — and a reminder 10 minutes before the slot, when joining opens. Send notify: false if you message the applicant yourself.
  • Open session_url for the applicant at the booked time (in a web view, or send it on). The hosted flow runs the workflow's steps and puts them in the call; a booked applicant is served ahead of walk-ups once the slot is due. An applicant who opens the link early can join straight away: they wait in the agents' queue — shown as waiting for their booked time — and an agent may take them before it (queue_state: "waiting").

Already created a session with POST /v3/session/? Book its Video Call step by sending session_id instead — purposeId and the applicant details become optional, nothing is charged, and the slot must start before that session expires.

Switching an existing integration

BeforeWith ThirdFactor
Base URLyour current backendhttps://<your-domain>/v3
Authentication—x-api-key: <tenant_api_key> on every call
purposeIdyour current purpose codeunchanged — give the workflow that code (Booking API purposes), or use its id from GET /v3/meeting-purpose
Request bodies, field names, date and time formats—unchanged
Responses—the same keys, with ThirdFactor's added alongside

Add an Idempotency-Key header to save while you're there — a fresh value per booking attempt makes a retried booking safe (see Idempotency).

Try it in the API Explorer

The API Explorer sends real requests to your tenant — put your API key in x-api-key, then:

  1. GET /v3/meeting-purpose — copy a purpose's id (e.g. 000000001).
  2. POST /v3/meeting-time/get-available — set purposeId to that id and date to a future working day within the next 14 days that your agents have availability on (DD/MM/YYYY). The example's date is only a sample: a past date, or a day nobody works, lists []. Copy an interval's id and time.
  3. POST /v3/meeting-request/save — same purposeId, the date as preferredDate, the interval's id as timeIntervalId and its time in 24-hour form as preferredTime (01:00 PM → 13:00). Leave Idempotency-Key empty, or give every attempt a fresh value — reusing one for a different booking is refused with 422 idempotency_key_reused.

List booking purposes

GET /v3/meeting-purpose
x-api-key: <tenant_api_key>
[
  {
    "id": "000000001",
    "workflow_id": "b2c1e9f0-4a3d-4c1b-9e2a-8f7d6c5b4a30",
    "name": "Video KYC",
    "timezone": "Asia/Kathmandu",
    "schedule_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "schedule_name": "Branch hours"
  }
]

Only workflows whose Video Call step allows appointment booking are listed (archived workflows never are). id is the numeric purposeId to send; workflow_id (the same purpose's UUID) is accepted too. schedule_id is null when no availability schedule governs the workflow — then only hand-published one-off slots are offered.

Get available time intervals

POST /v3/meeting-time/get-available
x-api-key: <tenant_api_key>
Content-Type: application/json
datestringbodyrequired

The day to list, DD/MM/YYYY, in the purpose's timezone (ISO YYYY-MM-DD is also read) — a future working day within the next 14 days.

purposeIdstringbodyrequired

An id from GET /v3/meeting-purpose: the 9-digit code (000000001; unpadded digits or a JSON number are read too) or the purpose's workflow_id. Optional when session_id is sent.

detailsbooleanbodyDefault: false

ThirdFactor extension: add time_24h, date, starts_at, ends_at, seats_left and timezone to every interval.

session_idstringbody

ThirdFactor extension: list what this verification session can book. Its own expiry then caps the list.

{
  "date": "05/10/2026",
  "purposeId": "000000001"
}
[
  { "id": "fe65740f-f776-4eec-a607-22c59fb3b1db", "time": "12:30 PM" },
  { "id": "374b53f3-d7ed-464c-ac0f-06e4167f2cf6", "time": "01:00 PM" },
  { "id": "5f4f786c-520f-4668-afd3-64087e80eaad", "time": "01:30 PM" }
]
[
  {
    "id": "374b53f3-d7ed-464c-ac0f-06e4167f2cf6",
    "time": "01:00 PM",
    "time_24h": "13:00",
    "date": "05/10/2026",
    "starts_at": "2026-10-05T13:00:00+05:45",
    "ends_at": "2026-10-05T13:30:00+05:45",
    "seats_left": 2,
    "timezone": "Asia/Kathmandu"
  }
]

The response is a bare JSON array of exactly {id, time}, soonest first — [] when nothing is free, exactly as the contract returns it. Send the chosen time to save as 24-hour preferredTime (01:00 PM → 13:00); with details: true, time_24h and date are those values ready-made.

An interval is listed only when it can really be booked:

  • Inside the agents' availability — generated from the purpose schedule's weekly windows, or published by hand as a one-off. Switching a schedule off stops its hours being offered.
  • Not started yet — past intervals, and intervals already under way today, are never listed.
  • With a seat left — capacity comes from the availability window. Several agents available at the same time appear once, with their seats summed in seats_left; booking that id takes whichever of them is free.
  • Within the booking horizon — slots are offered up to 14 days ahead. A date in the past or beyond the horizon returns [].

Availability an admin saves is bookable immediately — listed here, in the hosted flow's picker and on the console's slot list at once, with no refresh.

Book a time interval

POST /v3/meeting-request/save
x-api-key: <tenant_api_key>
Idempotency-Key: <a new UUID per booking attempt — optional>
Content-Type: application/json
fullnamestringbodyrequired

The applicant's full name, shown to agents. Optional with session_id, or when title carries the name (the contract sends it in both).

phonestringbody

The applicant's mobile number, 7–15 digits (+, spaces and dashes allowed). A phone or an email is required unless session_id is sent.

emailstringbody

The applicant's email. The confirmation and reminder go here when given, else by SMS to phone.

preferredDatestringbodyrequired

The date to book, DD/MM/YYYY, in the purpose's timezone — the date you listed.

preferredTimestringbodyrequired

The interval's start time, 24-hour HH:mm (13:00). The list's "01:00 PM" form is read too.

timeIntervalIdstringbodyrequired

An id from get-available.

purposeIdstringbodyrequired

The purpose's 9-digit code (or workflow_id) the interval was listed for. Optional with session_id; if sent with it, it must be that session's workflow.

titlestringbody

A label for the booking — in the contract, a copy of fullname. Defaults to fullname. Optional string fields may also be sent as null.

existingCustomerbooleanbodyDefault: false

Whether the applicant is already your customer — recorded on the booking and the session.

session_idstringbody

ThirdFactor extension: book the Video Call step of this existing verification session instead of creating one. sessionId is accepted too.

vendor_datastringbody

ThirdFactor extension: your user id for the session the booking creates — the same field as on POST /v3/session/.

notifybooleanbodyDefault: true

ThirdFactor extension: false sends no confirmation, reschedule notice or reminder — you message the applicant yourself.

{
  "email": "[email protected]",
  "existingCustomer": false,
  "fullname": "Sita Sharma",
  "phone": "9800000001",
  "preferredDate": "05/10/2026",
  "preferredTime": "13:00",
  "purposeId": "000000001",
  "timeIntervalId": "374b53f3-d7ed-464c-ac0f-06e4167f2cf6",
  "title": "Sita Sharma"
}
{
  "message": "Saved the Meeting Request data.",
  "status": "CREATED",
  "data": {
    "meetingRequestId": "000000324",
    "booking_id": "7f1c2a90-4b6d-4e15-9a02-3d8c1b7e5f44",
    "booking_status": "CONFIRMED",
    "queue_state": "scheduled",
    "applicant_waiting": false,
    "applicant_joined_at": null,
    "purposeId": "000000001",
    "workflow_id": "b2c1e9f0-4a3d-4c1b-9e2a-8f7d6c5b4a30",
    "purpose_name": "Video KYC",
    "timeIntervalId": "374b53f3-d7ed-464c-ac0f-06e4167f2cf6",
    "preferredDate": "05/10/2026",
    "preferredTime": "13:00",
    "time": "01:00 PM",
    "starts_at": "2026-10-05T13:00:00+05:45",
    "ends_at": "2026-10-05T13:30:00+05:45",
    "timezone": "Asia/Kathmandu",
    "session_id": "6f0e2b7a-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
    "session_url": "https://acme.v3.thirdfactor.ai/verify/Qm9va2luZy10b2tlbg",
    "applicant": {
      "fullname": "Sita Sharma",
      "email": "[email protected]",
      "phone": "9800000001",
      "title": "Sita Sharma",
      "existingCustomer": false
    },
    "video_call": {
      "id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
      "status": "waiting",
      "verdict": null
    },
    "cancelled_at": null,
    "cancel_reason": "",
    "created_at": "2026-09-24T08:15:02.118000+00:00",
    "updated_at": "2026-09-24T08:15:02.301000+00:00"
  }
}

status names the HTTP status — CREATED here, OK on the other calls; the booking's own state is data.booking_status. Treat any 2xx as success.

The time must be the interval's. preferredTime is checked against timeIntervalId in the purpose's timezone; a different time is 422 slot_mismatch, naming the interval's own start time in message and errors, so a mixed-up pick never books the wrong hour. An interval is a time of day: the same timeIntervalId with another preferredDate books that interval on that date (422 slot_mismatch on preferredDate when there is no such interval then). An id that is no longer offered — availability was edited since you listed — books the free interval at preferredDate + preferredTime instead; data.timeIntervalId always names the interval actually booked.

Seats are claimed atomically. Two requests racing for the last seat can't both win: the second gets 409 slot_full, and nothing — no booking, no session, no charge — is left behind. If the id you sent filled up but another agent is free at the very same time, that seat is booked instead (data.timeIntervalId tells you which).

Booking status

GET /v3/meeting-request/{meeting_request}
x-api-key: <tenant_api_key>

Returns {message: "Fetched the Meeting Request data.", status: "OK", data} with the same data as save. booking_status follows the call itself, so it also reflects what happened outside this API — the applicant moving or cancelling the appointment from their verification link, the agent taking the call, its outcome:

booking_statusMeaning
CONFIRMEDBooked; the seat is held.
RESCHEDULEDMoved to another interval — by you, or by the applicant from their link — and upcoming again.
IN_PROGRESSAn agent has picked the call up.
COMPLETEDThe agent gave a verdict: video_call.verdict is pass or fail.
NO_SHOWNo agent picked the call up by 30 minutes after the interval ended, or the call ended without a verdict.
CANCELLEDCancelled — by you (cancel_reason is yours) or by the applicant from their link (cancelled_in_hosted_flow).

CONFIRMED and RESCHEDULED are upcoming; IN_PROGRESS is live; COMPLETED, NO_SHOW and CANCELLED are final — except that a NO_SHOW whose call is still waiting can be rescheduled or cancelled.

queue_state says where the call stands for agents meanwhile:

queue_stateMeaning
scheduledBooked; the applicant isn't in the agents' queue yet.
waitingIn the agents' waiting queue — the applicant joined from their link (even before the slot: applicant_waiting: true, applicant_joined_at), or the slot is due. The booking itself stays CONFIRMED / RESCHEDULED.
in_callAn agent has the call (IN_PROGRESS).
completed / cancelled / no_showFinal, as booking_status.

An applicant still waiting in the queue after their slot ended is not a NO_SHOW — they came; the booking stays upcoming until an agent takes them.

The full verification outcome (documents, liveness, the agent's notes and recording) is on the session: GET /v3/session/{session_id}/decision/.

Reschedule

POST /v3/meeting-request/{meeting_request}/reschedule
x-api-key: <tenant_api_key>
Idempotency-Key: <unique-per-move>
Content-Type: application/json
{
  "preferredDate": "05/10/2026",
  "preferredTime": "14:30",
  "timeIntervalId": "2a99dc6d-cc5f-403d-b97a-0bcf81d79e39"
}

Pick the new interval from get-available and send it exactly as for save (optionally with notify, which then applies from here on). The old seat is released and the new one taken in one step — a refused move leaves the booking where it was. booking_status becomes RESCHEDULED and the applicant is told the new time. A session the booking created stays open until a day after the new slot.

Refused with 409: booking_not_modifiable when the booking is IN_PROGRESS, COMPLETED or CANCELLED (the body carries booking_status), slot_unchanged when it already holds that time, and slot_full / slot_in_past as for save.

Cancel

POST /v3/meeting-request/{meeting_request}/cancel
x-api-key: <tenant_api_key>
Content-Type: application/json
{ "reason": "Customer asked to cancel" }

Frees the seat and sets booking_status to CANCELLED; reason (optional, up to 255 characters) comes back as cancel_reason. If the booking created its own verification session and the applicant never opened it, that session is retired too — its link stops working and it expires like any unused session. A session you created yourself is left alone.

Status reads and cancellations keep working even if Video Call is later switched off for your tenant, so existing bookings can always be released. IN_PROGRESS, COMPLETED and already-CANCELLED bookings answer 409 booking_not_modifiable.

Idempotency

Send an Idempotency-Key header (any string up to 128 characters) on save, reschedule and cancel — a new value for every attempt, e.g. a UUID from uuidgen or crypto.randomUUID(). It is optional: leave it out (or empty in the API Explorer) when you don't need retry safety.

RetryResult
Same key, same bodyThe original response, verbatim — same status, same body — with Idempotent-Replayed: true. Nothing is booked, moved or charged again.
Same key, different body (or another endpoint)422 idempotency_key_reused — use a new key for a new operation. The usual cause when a second, different booking is refused right after a successful one.
Same key after a refusal (409 slot_full, 400, …)Runs again: only a successful request consumes a key.
Two concurrent requests, same keyOne books; the other waits for it and replays its response.

Keys are per tenant and remembered for the life of the booking. A replay returns the response as it was when first made; read GET /v3/meeting-request/{meeting_request} for the booking's current state.

Tip

Without a key, a save whose response you never received is not safe to repeat blindly — it can book a second seat and create a second session. With a key, retry as often as you like.

Errors

Every error — validation, business rules, authentication, throttling and malformed JSON alike — uses one envelope:

{
  "message": "That time interval is fully booked — fetch fresh slots and pick another.",
  "status": "CONFLICT",
  "detail": "slot_full",
  "data": null
}

Branch on the HTTP status first, then on detail — always a stable machine code. status names the HTTP status; message is a sentence you can log or show an operator. Validation errors add errors, a per-field map of messages; some refusals add context keys (features, booking_status, booking_id, meetingRequestId, retry_after).

HTTPdetailWhen
400validation_errorA missing field (fullname — or title — without session_id), a date that isn't DD/MM/YYYY, a time that isn't HH:mm, an invalid email or phone, a malformed id. See errors.
400invalid_jsonThe body isn't valid JSON.
401not_authenticated / authentication_failedNo x-api-key header / an unknown, revoked or suspended key.
402insufficient_creditsThe balance can't cover the session the booking creates.
403feature_not_enabledVideo Call is off for your tenant (features: ["VIDEO_CALL"]).
403api_key_tenant_mismatch / unknown_tenant_hostThe key was used on another tenant's base URL, or on a host that belongs to no tenant.
404purpose_not_foundpurposeId isn't one of your purposes (the message echoes it).
404slot_not_foundtimeIntervalId isn't a bookable interval of this purpose, and none starts at preferredDate + preferredTime.
405method_not_allowedWrong HTTP method (the booking calls are POSTs).
404session_not_foundsession_id isn't one of your sessions.
404booking_not_foundNo booking with that booking_id / meetingRequestId.
409slot_fullEvery seat at that time is taken.
409slot_in_pastThe interval has already started.
409session_already_bookedsession_id already holds a booking — reschedule or cancel it (its booking_id / meetingRequestId come back when it was booked here).
409session_closedThe session has finished or expired.
409call_in_progressAn agent is already on the session's call.
409booking_not_modifiableThe booking is in progress, completed or cancelled.
409slot_unchangedA reschedule to the time the booking already holds.
409idempotency_request_in_progressA request with the same key is still being processed — retry shortly.
422slot_mismatchpreferredTime isn't the start time of timeIntervalId, or there is no such interval on preferredDate.
422purpose_not_bookable / scheduling_not_enabledThe workflow's Video Call step doesn't allow appointment booking.
422purpose_session_mismatchpurposeId isn't the workflow of session_id.
422slot_after_session_expiryThe interval starts after session_id expires.
422applicant_not_eligibleThe applicant is blocklisted (by vendor_data), so no session can be created for them.
422idempotency_key_reusedThe key was already used for a different request — send a new key per booking.
429throttledRate limited — wait retry_after seconds (also the Retry-After header) and retry.

End to end

Find your purposeId

GET /v3/meeting-purpose once, and store the id (9-digit code) of the workflow you book for — or renumber the workflow to the code you already use.

Show the free times

POST /v3/meeting-time/get-available with the date the applicant picks; show each entry's time.

Book

POST /v3/meeting-request/save with a fresh Idempotency-Key, the applicant's details, the chosen entry's id, the date and its time in 24-hour form. Keep meetingRequestId and booking_id.

Send the applicant in at the booked time

Open session_url (or rely on the confirmation and reminder we send). Poll GET /v3/meeting-request/{booking_id} — or the session's decision — for the outcome.

curl -X POST "https://<your-domain>/v3/meeting-time/get-available" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -d '{"date": "05/10/2026", "purposeId": "000000001"}'

curl -X POST "https://<your-domain>/v3/meeting-request/save" \
  -H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"email": "[email protected]", "existingCustomer": false,
       "fullname": "Sita Sharma", "phone": "9800000001",
       "preferredDate": "05/10/2026", "preferredTime": "13:00",
       "purposeId": "000000001",
       "timeIntervalId": "374b53f3-d7ed-464c-ac0f-06e4167f2cf6",
       "title": "Sita Sharma"}'

Try every call live in the API Explorer under Slot Booking.