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.
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 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.
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"
]
}
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.
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
Authorization: Bearer <token>
{ "patientId": "pat_5GIQx5wzI0jX1waDkKNbfv", "slotId": "slt_5ilAAt7KAbWjrAw8rDXHzb" }
{
"code": "confirmation_required",
"confirmationToken": "9f8c2a1b4d6e4f0a8c1e2b3d4a5f6071",
"expiresAt": "2026-07-05T18:05:00Z",
"readBack": { "patientId": "pat_5GIQx5wzI0jX1waDkKNbfv", "slotId": "slt_5ilAAt7KAbWjrAw8rDXHzb" }
}
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.
| 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 dance. |
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.
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.
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 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 → 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 + confirmationToken → 201 |
readBack.
Parameters — the match request
Confirm the caller'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 | Caller's legal first name, e.g. John. |
| lastName | string | Required | PHI | Caller'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 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.
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
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.
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/capabilitiestrust 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_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 trace id so support can trace it; never read it to the patient.
{
"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"
}
type URL resolves to a page in the problem registry — thirteen codes, each with its HTTP status and recovery action.
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.
| 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 trace id. |
| 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. |
| upstream_unavailable | 502 | The scheduling system is temporarily unavailable. Transient — retry after a pause, then offer a callback. |
| capability_not_supported | 503 | This practice's scheduling system can't do this at all. Don't retry; tell the caller and hand off. |
| internal | 500 | Server error, 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/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
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 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
- The 428 confirmation protocol — the full dance 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, themed to match. This is a single-endpoint preview of the layout — the published reference renders the complete spec.
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.
| providerId integer | Required |
| locationId integer | Required |
| visitTypeId integer | Required |
| procedureId integer | Required |
| from string · date | Required |
| to string · date | Required |
| limit integer | Optional |
| cursor string | Optional |
slots:read
{
"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.
Downloads
Everything you need to wire an integration or brief a LLM-agent vendor.
agent guide · md Postman collection
sandbox · json Postman environment
sandbox · json openapi.json
v1 spec · json