A patient contacts your platform: "I'm looking for a Spanish-speaking virtual therapist who takes Sanitas." They don't know which provider they want. They can't. That's your job.
This is patient routing — and it's the problem most scheduling APIs don't solve.
The problem: routing before booking
Standard scheduling APIs — Calendly, Cal.com, Acuity — are built for a different use case. They assume you already know who the appointment is with. You get an event type ID, you fetch available slots, you book one. The routing step — figuring out who is eligible for this patient — is entirely up to you.
That works fine for single-host scheduling (one business, one calendar). It breaks immediately when you have a network of providers with different characteristics:
- Languages spoken
- Insurance accepted
- Modalities offered (in-person, virtual, hybrid)
- Whether they're accepting new clients
- Specialty, certification, or jurisdiction
When a patient has needs that span multiple of these dimensions, you're not doing simple calendar lookup anymore. You're doing constraint resolution across a provider graph.
What traditional scheduling APIs do
Here is how a typical scheduling API flow works:
1. Caller selects a provider (from a dropdown, a directory, a referral)
2. App fetches available slots for that provider
3. Caller picks a slot
4. App creates a booking
The assumption baked into step 1 is enormous: the caller already knows who they want. In consumer healthcare, coaching, legal intake, and most multi-provider platforms, that assumption fails.
The front-end workaround is usually a search page: show a directory, let the patient filter, let them pick a provider, then route them to a booking page. It works, but it puts the routing burden entirely on the patient. And it doesn't support AI-driven flows where an agent should determine the best match automatically.
The routing gap
The dimensions that matter for routing are different from the ones a calendar API understands. A calendar API knows about availability. It doesn't know about:
- Eligibility rules — does this provider accept this insurance? Are they licensed in this patient's state?
- Language — can this provider conduct sessions in Spanish?
- Modality — is the patient asking for virtual? In-person? The provider's schedule may not reflect this.
- Accepting-new-clients — a provider can have open slots but not be accepting new referrals.
- Preferences — the patient prefers female providers, or wants morning appointments, but would accept afternoon.
Routing logic that handles these dimensions can't be expressed as a simple slot lookup. It's a search problem with constraints and preferences.
Constraints vs preferences: the resolution model
The cleanest way to model multi-dimension routing is to separate hard constraints (must match) from soft preferences (nice to have):
Constraints eliminate providers who cannot serve this patient. A provider who doesn't speak Spanish is not an option, regardless of availability. A provider who doesn't accept Sanitas is not an option.
Preferences rank the remaining options. If two providers are equally eligible, prefer the one with morning slots. If the patient prefers virtual, surface virtual-first — but don't eliminate in-person if that's the only eligible option.
The Orita API models this directly through the resolveScheduling call:
import Orita from 'orita-sdk';
const orita = new Orita({ apiKey: process.env.ORITA_API_KEY });
const resolution = await orita.resolveScheduling({
serviceId: 'svc_therapy_individual',
constraints: {
languageCodes: { anyOf: ['es'] }, // hard — must speak Spanish
insuranceCodes: { anyOf: ['sanitas'] }, // hard — must accept Sanitas
modalityCodes: { anyOf: ['virtual'] }, // hard — must offer virtual
acceptsNewClients: true, // hard — must be accepting
},
preferences: {
timeOfDay: 'morning', // soft — prefer mornings
},
dateRange: {
from: '2026-08-04',
to: '2026-08-11',
},
});
The API handles the resolution on the server: it evaluates the full provider graph against the constraints, finds the eligible intersection with availability, and returns ranked options.
What the response looks like
The resolution response doesn't just return slots — it returns explained options:
{
"resolutionId": "res_abc123",
"status": "resolved",
"options": [
{
"optionId": "opt_001",
"score": 0.94,
"provider": {
"id": "pro_maria_garcia",
"name": "MarÃa GarcÃa",
"languageCodes": ["es", "en"],
"modalityCodes": ["virtual"]
},
"slot": {
"startUtc": "2026-08-05T09:00:00Z",
"durationMinutes": 50,
"modality": "virtual"
},
"matchedConstraints": [
"languageCodes:es",
"insuranceCodes:sanitas",
"modalityCodes:virtual",
"acceptsNewClients:true"
],
"reason": "Only available Spanish-speaking virtual therapist accepting new Sanitas patients this week",
"exclusions": []
}
]
}
Three things here that matter for production applications:
-
matchedConstraints— confirms which rules this option satisfies. Useful for displaying to the patient ("We found a therapist who matches your requirements") and for audit trails. -
reason— a human-readable explanation. If you're building an AI receptionist, this becomes the script: "I found MarÃa GarcÃa, she's available Tuesday morning, speaks Spanish, and accepts Sanitas." -
exclusions— if the resolution came back empty (no eligible providers), this field tells you why each provider was excluded. Essential for debugging and for communicating to the patient that no match was found.
The confirm step — and why it's separate
After presenting options to the patient, you confirm the booking as a separate step:
const booking = await orita.confirmResolution(resolution.resolutionId, {
optionId: 'opt_001',
customer: {
name: 'Ana López',
email: 'ana@example.com',
phone: '+34600000000',
},
});
The confirm step is separate from resolve for good reasons:
- Race conditions. Between resolve and confirm, the slot may have been taken by another patient. The confirm step handles this atomically — if the slot is gone, it returns an error rather than creating a double-booking.
- Patient approval. In any booking flow involving a patient, there should be a human-visible confirmation step. Separating resolve and confirm makes this natural: resolve returns options, the patient (or the AI on their behalf) approves, then confirm executes.
- Idempotency. The confirm call is idempotent. If your network times out and you retry, you get the same booking back — not a duplicate.
When to use this vs a simple booking API
Use a resolution-based approach (like resolveScheduling) when:
- You don't know which provider the patient should see
- Eligibility rules determine who can serve this patient
- You're building an AI agent that should pick the best match autonomously
- You need to explain the recommendation to the patient or auditor
Use a simple slot-booking API (Calendly, Cal.com, etc.) when:
- The caller already knows which provider they want
- You're building a scheduling widget for a single host
- No eligibility filtering is required
The majority of consumer-facing healthcare, coaching, legal, and expert marketplace applications fall in the first category. The routing problem is the hard part — and it's the part that most scheduling APIs don't help with.
Start routing
Orita's resolveScheduling endpoint is available on the free tier. Get your API key, configure a provider or two with their constraints, and call resolve with a patient need.
Questions? hola@orita.online
