Updated 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 — a deliberate-commit gate between the agent and the API, so a write can never fire on a single mistaken or hallucinated call. Every error is an RFC 7807 problem+json body with a stable machine code. Currently supported practice-management systems: Nextech Select and Modernizing Medicine.

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 reveal them to the patient.

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"
  ]
}
LLM agents

Connect via MCPnew

Call the same operations as tools over the Model Context Protocol — for agent runtimes that connect and invoke, instead of composing HTTP requests.

This platform is also a Model Context Protocol server. It's built to be wired into a third-party service's own agent — that service's MCP client connects once and calls each operation as a self-describing tool, instead of composing HTTP requests. The auth, the practice-scoped token, and the 428 confirmation flow are identical to the HTTP API; only the transport differs. This page covers what's MCP-specific — everything else lives in the sections it links to.

Auth and tenancy are the same as HTTP — get a token from the authentication quickstart, and read the 428 confirmation protocol before any mutating tool. The server validates your bearer and forwards it to the API unchanged — it mints nothing.

Get a token

POST your {{CLIENT_ID}} and {{CLIENT_SECRET}} to {{OAUTH_TOKEN_URL}} using the OAuth2 client-credentials grant — the same token the HTTP API takes, shown in the panel. Tokens are short-lived and there are no refresh tokens, so an agent should mint on connect and re-mint when expires_in lapses, rather than paste a fixed value. The authentication quickstart covers scopes and the get_capabilities trust check in full.

Point your client at the endpoint

Connect to {{MCP_BASE_URL}}/mcp — stateless Streamable HTTP — sending the token on a static Authorization: Bearer connection header.

Discover the tools

The server advertises one tool per operation; each is self-describing via tools/list, so you don't hand-code the surface. Call get_capabilities first to see the scopes your token holds. The full operation set is the same one in the API reference.

Confirm before writing

A mutating tool returns a non-error result describing the pending change plus a confirmationToken. To execute, call the same tool again with the same arguments plus that token — the MCP form of "resend with the token". The server never commits on the first call. Both calls are machine-to-machine and your client makes the second one itself; the patient's go-ahead belongs before the first, not between the two. See the confirmation protocol.

Tool errors carry the same RFC 9457 problem codes as the HTTP API (e.g. validation, forbidden_scope) — so an agent knows exactly what to fix, not merely that something failed.
Token Client credentials
POST {{OAUTH_TOKEN_URL}}
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={{CLIENT_ID}}
&client_secret={{CLIENT_SECRET}}

# 200 — send access_token as the bearer below
{
  "access_token": "<token>",
  "token_type": "Bearer",
  "expires_in": 450
}
Config Generic MCP client
{
  "type": "url",
  "url": "{{MCP_BASE_URL}}/mcp",
  "name": "rsi",
  "authorization_token": "<bearer token>"
}
Wire What your client sends
# any MCP client library does this for you — shown raw for clarity
curl -X POST {{MCP_BASE_URL}}/mcp \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# → SSE frame:  data: {"result":{"tools":[ …22 tools… ]}}
tool book_appointment · confirm
# 1) call the tool — no confirmationToken
book_appointment(patientId, slotId)
# → non-error result: "CONFIRMATION REQUIRED … confirmationToken=…"

# 2) same tool, same args, plus the token → executes
book_appointment(patientId, slotId, confirmationToken)
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 patient's go-ahead happens before this and is your responsibility; the API neither sees nor verifies that conversation.

Every write is two-phase — there are no exceptions: book an appointment, cancel an appointment, confirm attendance, create a patient, update a patient. Reads are always a single call.

The exchange, end to end

Two steps, both machine-to-machine

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 and attendance confirm, the token rides in the otherwise-optional body. Your agent sends this itself, straight after the challenge — there is no human step between the two calls.

Before you confirm — obtaining the patient's go-ahead is your responsibility, not the API's, and it happens out-of-band before this exchange. The 428 readBack is a machine echo for you to check your own intent against, not a script for the patient: for a booking the human read-back is the earlier GET /api/slots/{id} (provider, location, time); for a new patient the readBack's name and DOB double as that read-back. Never read patientId, slotId, appointmentId, or the token to the patient.

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

{ "patientId": "{patientId}", "slotId": "{slotId}",
  "confirmationToken": "{confirmationToken}" }

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

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 exchange.
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

Getting the patient's "yes" is your responsibility and is separate from the 428 — the two-phase step is a machine safeguard the API runs regardless of any conversation. For a booking, read the human details from GET /api/slots/{id} back to the patient and get their agreement before you commit. The readBack is a machine checksum, not a script.

Guides

Booking playbook

The scripted, test-verified call flow — turning a patient'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 patient's phrases ("Dr Smith, Downtown, for acne") into those ids — opaque encoded strings — against the directory endpoints, by name. Never relay any of these ids to the patient.

Patient 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
→ the returned id is your patientId (404 = no match)
2"At the Downtown clinic…" List locations, match the patient'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, visit type and procedure, 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 read the details back, so resend identically with the token — no second question to the patient.
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 patient's confirmation is step 7's read-back of names and times, not the 428 readBack.

Parameters — the match request

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

NameTypeRequiredPHIDescription
firstNamestring Required PHI Patient's legal first name, e.g. John.
lastNamestring Required PHI Patient'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 returns one patient (its id is your patientId); a 404 means you have not verified them — offer to create a new patient or take a message, never read back records.

LLM agents

SKILL.md

The operating manual for agents that compose HTTP requests themselves — hand it to the model and it holds the whole contract. Runtime speaks MCP instead? Connect via MCP.

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 exchange, 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 an automated client 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 patient's identity by match before disclosing anything. Never relay ids, tokens, or MRNs to the patient. 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, 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 exchange 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 requestId so support can trace it; never read it to the patient.

422 application/problem+json example
{
  "type": "https://docs.redspotinteractive.com/errors.html#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 that code's anchor in the error catalog — every code the platform emits, with its HTTP status, meaning, and retry semantics. The problem registry summarizes the recovery action per code.
Reference

Problem registry

Every error body carries a type URL that resolves to the code's entry in the error catalog.

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.

12 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 requestId.
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.
capability_not_supported503This practice's scheduling system can't do this at all. Don't retry; tell the patient and hand off.
internal500Server error, the scheduling system is unreachable, 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/errors.html#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 patient, 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.

Every endpoint below is rendered live from openapi.json — the same spec APIM imports, so it always matches the deployed API. See the booking playbook for the end-to-end 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.