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.
Authentication
OAuth2 client credentials. The token carries your practice, scopes, and grant type — you never pass a tenant id.
QuickstartThe 428 confirmation protocol
Every mutation is challenge-then-confirm. Resend the identical body plus the token to commit.
Read the protocolBooking playbook
The eight-step call flow: turn "Dr Smith, Downtown, for acne" into four ids and a slot.
Follow the flowError registry
Thirteen stable codes, each with an HTTP status and a recovery action. Match on code, not text.
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.
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 }
curl {{API_BASE_URL}}/api/capabilities \
-H "Authorization: Bearer <access_token>"
# 200
{ "capabilities": ["patients:read", "practices:read", "slots:read", "appointments:book"] }
Resources
Everything you need to wire up an integration or hand a spec to a LLM-agent vendor.
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.
forbidden_tenant.
POST {{OAUTH_TOKEN_URL}}
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id={{CLIENT_ID}}
&client_secret={{CLIENT_SECRET}}
GET {{API_BASE_URL}}/api/capabilities
Authorization: Bearer <token>
# 200
{
"capabilities": [
"patients:read",
"slots:read",
"appointments:book"
]
}
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.
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.
validation, forbidden_scope) — so an agent knows exactly what to fix, not merely that something failed.
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
}
{
"type": "url",
"url": "{{MCP_BASE_URL}}/mcp",
"name": "rsi",
"authorization_token": "<bearer token>"
}
# 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… ]}}
# 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)
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.
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
Authorization: Bearer <token>
{ "patientId": "{patientId}", "slotId": "{slotId}" }
{
"code": "confirmation_required",
"confirmationToken": "{confirmationToken}",
"expiresAt": "2026-07-05T18:05:00Z",
"readBack": { "patientId": "{patientId}", "slotId": "{slotId}" }
}
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.
| Situation | Result | What 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. |
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.
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.
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 says | You do | API 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 + confirmationToken → 201 |
readBack.
Parameters — the match request
Confirm the patient's identity before disclosing anything. All four fields are required; phone needs at least four digits.
| Name | Type | Required | PHI | Description |
|---|---|---|---|---|
| firstName | string | Required | PHI | Patient's legal first name, e.g. John. |
| lastName | string | Required | PHI | Patient's legal last name, e.g. Doe. |
| phone | string | Required | PHI | Contact number, at least 4 digits. Matched on a normalized digit substring. |
| dob | string | 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.
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
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.
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/capabilitiestrust 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_incompleterecovery. - 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.
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.
{
"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"
}
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.
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.
| Code | HTTP | Meaning |
|---|---|---|
| confirmation_mismatch | 409 | Confirm body differs from the challenged intent — start over with a fresh challenge. |
| unauthorized | 401 | No, invalid, or expired token. Refresh and retry once; if it recurs, hand off. |
| forbidden_scope | 403 | Token lacks the endpoint's scope. Don't promise the action; check /api/capabilities. |
| forbidden_tenant | 403 | Token's practice couldn't be resolved. Configuration problem — escalate with the requestId. |
| not_found | 404 | Unknown or cross-practice resource. Re-confirm identity or re-run the lookup; never assume another patient. |
| validation | 422 | Bad/missing parameters (errors[]), or the scheduling system declined a write. Fix the named field and retry. |
| confirmation_required | 428 | The two-phase challenge. Not an error — resend identically with the confirmationToken. |
| confirmation_incomplete | 409 | Token consumed, outcome unknown. Verify via the read endpoints before any retry. |
| slot_unavailable | 409 | The slot was taken between search and book. Re-run GET /api/slots and offer the next times. |
| conflict | 409 | Generic conflicting state. Re-read the resource, reconcile, retry. |
| capability_not_supported | 503 | This practice's scheduling system can't do this at all. Don't retry; tell the patient and hand off. |
| internal | 500 | Server error, the scheduling system is unreachable, or a write whose audit couldn't be recorded. For a mutation, verify state before retrying. |
confirmation_mismatch 409
The confirming request's body differs from the intent that was challenged, so nothing executes.
{
"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
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.
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
- The 428 confirmation protocol — the full exchange and its edge cases.
confirmation_incomplete— token consumed, outcome unknown (a different 409).slot_unavailable— the slot was taken between search and book.
API reference
The full v1 endpoint surface — providers, patients, slots, appointments, and the directory.
openapi.json — the same spec APIM imports, so it always matches the deployed API. See the booking playbook for the end-to-end sequence.
Downloads
Everything you need to wire an integration or brief a LLM-agent vendor.
agent guide · md Postman collection
sandbox/prod · json Postman environment
sandbox · json openapi.json
v1 spec · json