Version 2026-07-05

RSI Public Platform API

Medical scheduling for practice-management integrations and LLM agents. v1.

Find open slots, book, confirm, and cancel appointments, and manage patient records against a single medical practice — every call authenticated with a practice-scoped bearer token. Reads are one request; mutations run a two-phase 428 confirmation so an agent can read the details back to the patient before anything is committed. Every error is an RFC 7807 problem+json body with a stable machine code.

Your first call in 60 seconds

Exchange your client credentials for a bearer token, then prove it works against /api/capabilities — the one endpoint any valid token may call.

Step 1 Request a token · client credentials
curl -X POST {{OAUTH_TOKEN_URL}} \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id={{CLIENT_ID}}" \
  -d "client_secret={{CLIENT_SECRET}}"

# 200 → { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600 }
Step 2 Prove the token · echo your scopes
curl {{API_BASE_URL}}/api/capabilities \
  -H "Authorization: Bearer <access_token>"

# 200
{ "capabilities": ["patients:read", "practices:read", "slots:read", "appointments:book"] }
Access is invitation-only. Have credentials? Start with the authentication quickstart.

Resources

Everything you need to wire up an integration or hand a spec to a LLM-agent vendor.

Get Started

Authentication quickstart

Four steps from a client-credentials pair to your first authenticated read.

Receive your credentials

Access is invitation-only. RSI provisions a {{CLIENT_ID}} and {{CLIENT_SECRET}} for your practice and injects them from secure config. Never log them and never speak them aloud on a call.

Request a token

Exchange the credentials at {{OAUTH_TOKEN_URL}} using the OAuth2 client-credentials grant. The response is a bearer token valid for expires_in seconds — send it on the Authorization header of every request.

Make your first read

GET /api/capabilities echoes the token's granted scopes. Any valid token may call it — use it to prove connectivity and to check you actually hold a scope before promising an action. A missing scope means don't offer that action; hand off instead.

Where next

Mutations aren't single calls on your grant — read the 428 confirmation protocol before you book. Then work the API reference for the full endpoint surface.

Tokens are practice-scoped. The API resolves your practice from the token automatically — you never pass a practice or tenant id yourself. A token whose practice can't be resolved returns 403 forbidden_tenant.
Steps 1–2 POST token
POST {{OAUTH_TOKEN_URL}}
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={{CLIENT_ID}}
&client_secret={{CLIENT_SECRET}}
Step 3 GET /api/capabilities
GET {{API_BASE_URL}}/api/capabilities
Authorization: Bearer <token>

# 200
{
  "capabilities": [
    "patients:read",
    "slots:read",
    "appointments:book"
  ]
}
Get Started

The confirmation protocol 428

Every mutation is two-phase: the API returns a challenge, then an identical confirming request commits it.

This is a safety gate between your agent and the API — it forces a mutation to be committed deliberately, in two steps, so a booking, cancel, or patient write can never fire on a single (possibly mistaken) call. Because writes on your client require confirmation, your first attempt returns 428 with a confirmationToken and a readBack instead of performing the action. You resend the identical body to the same URL with that token, and the phase flips from challenge to commit. No separate endpoint, no state to negotiate — the token's presence is the only thing that changes. Getting the caller's go-ahead happens before this and is your responsibility; the API neither sees nor verifies that conversation.

The dance, end to end

Two steps

Challenge

POST the action with your normal body and no confirmationToken. You get 428 application/problem+json carrying a 32-hex-character token, an expiresAt, and a readBack — the machine's echo of what it understood. This is expected, not an error.

Confirm

Resend the identical body plus the confirmationToken to the same URL. You get the real result — 201 or 200. For cancel, the token rides in the otherwise-optional body.

Before you confirm — obtaining the caller's go-ahead is your responsibility, not the API's. Use readBack to confirm the human-meaningful details: provider, location, and time for a booking; name and DOB for a new patient. Never read patientId, slotId, appointmentId, or the token aloud.

The token, threaded verbatim

The token is bound to a fingerprint of your exact fields. Watch the highlighted value — it is minted in the 428 response and carried, character for character, into the confirming request.

POST /api/appointments phase 1 · no token
POST /api/appointments
Authorization: Bearer <token>

{ "patientId": "pat_5GIQx5wzI0jX1waDkKNbfv", "slotId": "slt_5ilAAt7KAbWjrAw8rDXHzb" }
428 application/problem+json the challenge
{
  "code": "confirmation_required",
  "confirmationToken": "9f8c2a1b4d6e4f0a8c1e2b3d4a5f6071",
  "expiresAt": "2026-07-05T18:05:00Z",
  "readBack": { "patientId": "pat_5GIQx5wzI0jX1waDkKNbfv", "slotId": "slt_5ilAAt7KAbWjrAw8rDXHzb" }
}
carried verbatim from the challenge response
POST /api/appointments phase 2 · identical body + token
POST /api/appointments
Authorization: Bearer <token>

{ "patientId": "pat_5GIQx5wzI0jX1waDkKNbfv", "slotId": "slt_5ilAAt7KAbWjrAw8rDXHzb",
  "confirmationToken": "9f8c2a1b4d6e4f0a8c1e2b3d4a5f6071" }

# 201 → { "appointmentId": "apt_0AYXEZeALa6T4t9NAfrufC" }

Edge cases

Codes link to their page in the problem registry.

SituationResultWhat it means
Resend the same token + same body after it already succeeded 200 / 201 replay Returns the stored result (same appointmentId) — it does not double-book. Safe blind retry after a network blip.
A business field changed between challenge and confirm 409 confirmation_mismatch Nothing executes; the original intent stays valid until it expires. Re-read state and issue a fresh challenge with the corrected body.
Token consumed, but the outcome wasn't recorded 409 confirmation_incomplete Do not blindly retry. Verify current state via the read endpoints; if it already happened, proceed, otherwise start a fresh challenge.
Token's expiresAt has passed (or the server doesn't recognize it) 428 fresh challenge You get a brand-new token — use the new one and continue the dance.
Integrator

Persist the token from the 428 and resend the identical body. Never mutate a challenged body between phases — any change invalidates the fingerprint and returns a 409.

Agent

Read the human details — provider, location, day, time — back to the caller and get a "yes" before you send the confirming request. The readBack is a machine echo, not your script.

Guides

Booking playbook

The scripted, test-verified call flow — turning a caller's words into four ids and a confirmed slot.

Slot search needs all four of providerId, locationId, visitTypeId, and procedureId — none is optional — plus a date window of 14 days or less. So most of the work is resolving the caller's phrases ("Dr Smith, Downtown, for acne") into those numeric ids against the directory endpoints, by name. Never speak any of these ids to the caller.

Caller saysYou doAPI call
1"I want to book an appointment — yes, I'm an existing patient." Confirm identity by demographic match. Collect first name, last name, phone, DOB.
POST /api/patients/match
→ take items[].id as patientId
2"At the Downtown clinic…" List locations, match the caller's phrase to a name.
GET /api/locations
→ match name → locationId
3"…with Dr Smith…" List providers at that location, match by name (alias-aware, e.g. "Dr. Bob").
GET /api/providers?locationId=
providerId
4"Yes, existing patient." Pick the visit type by new-vs-existing status. Names read like "New Patient" / "Established Patient".
GET /api/visit-types
→ match name → visitTypeId
5"…for acne." List procedures bookable under that visit type, match the concern. If several match, ask one clarifying question.
GET /api/visit-types/{id}/procedures
procedureId
6"In a couple of days, around 10:30am." Search slots with all four ids + a from/to window (yyyy-MM-dd, ≤ 14 days). Pick the closest time.
GET /api/slots
?providerId=&locationId=&visitTypeId=&procedureId=&from=&to= → items[].id as slotId
7(you're about to commit) Read the slot back verbally — provider, location, type, day/time — then optionally re-check live availability.
GET /api/slots/{slotId}
availability answers live — open slots re-check in the scheduling system inside this read
8"Yes, book it." Book. The first POST returns 428; you already spoke the details, so resend identically with the token.
POST /api/appointments
{patientId, slotId} → 428 → resend + confirmationToken201
Procedures hang off visit types — there is no per-procedure back-link. Always resolve the visit type first (step 4), then fetch its procedures (step 5). The spoken confirmation is step 7's read-back of names and times, not the 428 readBack.

Parameters — the match request

Confirm the caller's identity before disclosing anything. All four fields are required; phone needs at least four digits.

NameTypeRequiredPHIDescription
firstNamestring Required PHI Caller's legal first name, e.g. John.
lastNamestring Required PHI Caller's legal last name, e.g. Doe.
phonestring Required PHI Contact number, at least 4 digits. Matched on a normalized digit substring.
dobstring Required PHI Date of birth, yyyy-MM-dd — e.g. 1985-03-15. Date-only; a full datetime is rejected.

A match with items confirms the caller; no items means you have not verified them — offer to create a new patient or take a message, never read back records.

Guides

For LLM agents

A single self-contained contract you can hand to a LLM-agent runtime — the source of truth behind these docs.

SKILL.md is the machine-facing guide for an LLM agent in a live patient conversation, acting for one medical practice. It packs the whole flow — OAuth setup, the id-acquisition the slot search requires, the mandatory two-phase 428 dance, the full endpoint surface, every error code with recovery steps, and the HIPAA and conversational rules — into one document written to be read by the model itself.

Two rules dominate everything

Two-phase mutations

You are a machine caller with a confirmation_required grant. Every mutating call is two-phase: the first attempt returns 428 with a confirmationToken and a readBack; you resend the identical body plus that token to perform the action. 428 is a normal protocol step, not an error.

HIPAA minimum-necessary

Confirm the caller's identity by match before disclosing anything. Never read ids, tokens, or MRNs aloud. Read back names, dates, and times. Never list or reveal another patient's data.

What it covers

  • Setup — base URL, OAuth2 client-credentials, and the /api/capabilities trust check.
  • The conversation playbook — the canonical 8-step booking dialogue, plus cancel, one-shot attendance confirm, and patient create/update.
  • Endpoint reference — every path, scope, parameter, and response shape, all camelCase JSON with null fields omitted.
  • The 428 protocol — the dance and all its edge cases: byte-identical resend, safe replay, expiry, confirmation_incomplete recovery.
  • Error handling — the full code table with per-code recovery steps.
  • Conversational & HIPAA rules — identity-first, never speak ids, read back before every commit, one clarifying question at a time.
Get Started

Error handling

Every error is an RFC 7807 problem+json body with a stable machine code. Match on the code, never the prose.

Errors carry a type URL, a title, an HTTP status, a stable code, and a requestId, alongside an X-RSI-TRACE-ID response header. The title and detail are human strings and may carry no PHI by design — branch on code. On any escalation, quote the trace id so support can trace it; never read it to the patient.

422 application/problem+json example
{
  "type": "https://docs.redspotinteractive.com/api/problems/validation",
  "title": "One or more parameters are invalid.",
  "status": 422,
  "code": "validation",
  "errors": [ { "field": "to", "rule": "maxWindow", "message": "Window must be ≤ 14 days." } ],
  "requestId": "req_5f2c9a"
}
Each type URL resolves to a page in the problem registry — thirteen codes, each with its HTTP status and recovery action.
Reference

Problem registry

Every error body carries a type URL that resolves to one of these pages.

Branch your handling on the stable code, not on the HTTP status alone — several codes share a status (three distinct 409s, two 403s). The recovery column is the action a LLM agent should take on the call.

13 codes
CodeHTTPMeaning
unauthorized401No, invalid, or expired token. Refresh and retry once; if it recurs, hand off.
forbidden_scope403Token lacks the endpoint's scope. Don't promise the action; check /api/capabilities.
forbidden_tenant403Token's practice couldn't be resolved. Configuration problem — escalate with the trace id.
not_found404Unknown or cross-practice resource. Re-confirm identity or re-run the lookup; never assume another patient.
validation422Bad/missing parameters (errors[]), or the scheduling system declined a write. Fix the named field and retry.
confirmation_required428The two-phase challenge. Not an error — resend identically with the confirmationToken.
confirmation_incomplete409Token consumed, outcome unknown. Verify via the read endpoints before any retry.
slot_unavailable409The slot was taken between search and book. Re-run GET /api/slots and offer the next times.
conflict409Generic conflicting state. Re-read the resource, reconcile, retry.
upstream_unavailable502The scheduling system is temporarily unavailable. Transient — retry after a pause, then offer a callback.
capability_not_supported503This practice's scheduling system can't do this at all. Don't retry; tell the caller and hand off.
internal500Server error, or a write whose audit couldn't be recorded. For a mutation, verify state before retrying.
Reference · Problem registry

confirmation_mismatch 409

The confirming request's body differs from the intent that was challenged, so nothing executes.

409 application/problem+json
{
  "type": "https://docs.redspotinteractive.com/api/problems/confirmation_mismatch",
  "title": "The confirmation does not match the challenged request.",
  "status": 409,
  "code": "confirmation_mismatch",
  "requestId": "req_5f2c9a1b"
}

Why this happens

  • A business field changed between the challenge and the confirm — the token is bound to a fingerprint of your exact fields, so any drift invalidates it.
  • A second, different intent reused a token that was minted for the first one.

Nothing executes, and the original challenged intent stays valid until its expiresAt passes.

How to resolve

Integrator

Re-read current state, then issue a fresh challenge (no token) with the corrected body. Never mutate a body that has already been challenged — persist it verbatim between the two phases.

Agent

Re-confirm the details with the caller, then start a fresh booking attempt. Don't try to patch the in-flight one — begin a new challenge for the corrected intent.

Related

Reference

API reference

The full v1 endpoint surface — providers, patients, slots, appointments, and the directory.

Generated by ReDoc from openapi.json, themed to match. This is a single-endpoint preview of the layout — the published reference renders the complete spec.
GET /api/slots

Search open slots

Returns bookable slots for a provider at a location, for a visit type and procedure, within a date window. All four selectors are required and must be greater than zero; the window must be yyyy-MM-dd with from ≤ to and no more than 14 days.

Query parameters
providerId integerRequired
locationId integerRequired
visitTypeId integerRequired
procedureId integerRequired
from string · dateRequired
to string · dateRequired
limit integerOptional
cursor stringOptional
Scope

slots:read

Response · 200
{
  "items": [
    {
      "id": "slt_5ilAAt7KAbWjrAw8rDXHzb",
      "start": "2026-07-07T10:30:00-05:00",
      "end": "2026-07-07T11:00:00-05:00",
      "providerId": 201,
      "locationId": 101,
      "visitTypeId": 401,
      "procedureId": 301
    }
  ],
  "limit": 20
}

Read it back with GET /api/slots/{slotId} for the provider and location names — its available answers live (open slots are re-checked in the scheduling system inside the read) — then book via POST /api/appointments. See the booking playbook for the full sequence.

Resources

Downloads

Everything you need to wire an integration or brief a LLM-agent vendor.

Access is invitation-only. These artifacts assume you already hold client credentials — see the authentication quickstart to exchange them for a token.