When your AI assistant books a therapy session, a lot happens between "find me something Tuesday afternoon" and the confirmation email landing in the user's inbox. Orita splits this into two deliberate steps β resolve and confirm β with an optional hold in between. This post walks through the entire flow, explains why each step exists, and gives you complete working code in both JavaScript and Python.
Why Two Steps?
Most scheduling APIs take a single "book this slot" call. Orita doesn't, and for a good reason.
Your AI agent needs to present options to the user before committing. The user might want to see three available therapists, compare their profiles, or simply think for 30 seconds before deciding. If you booked on the first call, you'd either waste a slot or force a cancellation-and-rebook every time the user changed their mind.
The split also makes the system more resilient: resolveScheduling is a read-heavy operation that can be retried freely, while confirmResolution is the single atomic write that creates the booking. Separating them means you never accidentally double-book.
resolveScheduling() β Present options to user β holdOption() β confirmResolution()
(read) (UI step) (optional) (write)
Step 1: Resolve β Find Available Options
Call resolveScheduling with the service name, constraints, date range, and preferences. Orita queries your connected provider network and returns ranked options.
JavaScript / TypeScript
import Orita from "orita-sdk"; // orita-sdk@1.3.0
const orita = new Orita({ apiKey: process.env.ORITA_API_KEY });
const resolution = await orita.resolveScheduling({
service: "therapy",
constraints: {
languageCodes: { anyOf: ["es"] }, // Spanish-speaking provider required
modalityCodes: { anyOf: ["virtual"] }, // Telehealth only
},
dateRange: {
from: "2026-08-05",
to: "2026-08-12",
},
preferences: {
dayParts: ["afternoon"], // Soft preference β not a hard filter
},
});
console.log(resolution.resolutionId); // "res_01j..."
console.log(resolution.status); // "resolved" | "zero_results"
console.log(resolution.summary); // Human-readable summary
console.log(resolution.options); // Ranked list of available slots
Python
import orita # pip install orita-sdk
client = orita.Client(api_key=os.environ["ORITA_API_KEY"])
resolution = client.resolve_scheduling(
service="therapy",
constraints={
"languageCodes": {"anyOf": ["es"]},
"modalityCodes": {"anyOf": ["virtual"]},
},
date_range={"from": "2026-08-05", "to": "2026-08-12"},
preferences={"dayParts": ["afternoon"]},
)
print(resolution["resolutionId"]) # "res_01j..."
print(resolution["options"]) # list of ranked slot options
Understanding the Response
Each item in resolution.options looks like this:
{
"optionId": "opt_abc123",
"slotId": "slot_xyz789",
"provider": {
"id": "prov_456",
"name": "Dr. Ana Torres",
"specialty": "Cognitive Behavioral Therapy"
},
"slot": {
"start": "2026-08-06T15:00:00-05:00",
"end": "2026-08-06T16:00:00-05:00",
"modality": "virtual"
},
"score": 0.92,
"reason": "Strong language and modality match; afternoon preference met"
}
Key fields:
resolutionIdβ Stored server-side. Expires in 5 minutes. After expiry, any downstream call returns410 Gone.optionIdβ Identifies a specific option within this resolution. Use it for hold and confirm.slotIdβ The underlying slot on the provider's calendar.scoreβ 0β1 ranking. Higher is a better match. Show these in descending order in your UI.reasonβ Human-readable explanation. Useful for explainable AI UIs.
If the network returns nothing (options: []), check resolution.status === "zero_results" and inspect zeroResultReasons β see our analytics guide for how to diagnose this.
Step 2 (Optional): Hold the Option
Once your user is looking at the options list, you have a race condition: another user could book the same slot before yours confirms. For high-stakes scenarios β therapy sessions, one-on-one consultations, anything where the same slot genuinely has contention β place a hold.
const hold = await orita.holdOption(
resolution.resolutionId,
resolution.options[0].optionId,
120 // TTL in seconds β hold expires after 2 minutes
);
// { holdId: "hold_...", status: "active", expiresAt: "2026-08-06T15:02:00Z" }
hold = client.hold_option(
resolution_id=resolution["resolutionId"],
option_id=resolution["options"][0]["optionId"],
ttl_seconds=120,
)
While the hold is active, the slot is reserved for this resolution only. Other resolution attempts will not include this slot in their options.
What happens when the hold expires?
The slot becomes available again automatically. The hold TTL is a hard expiry β no grace period. If your user is still deciding at second 121, the slot could be taken before they confirm. Design your UI accordingly: show a visible countdown timer and disable the "Book" button when the hold expires, prompting the user to re-search.
Step 3: Confirm β Create the Booking
When the user clicks "Confirm", call confirmResolution. This single call:
- Re-validates slot availability (even if you held it β the hold is released atomically)
- Creates the booking record
- Emits the
booking.createdwebhook to your endpoints - Returns the booking
idandstatus: "pending"
const booking = await orita.confirmResolution(
resolution.resolutionId,
{
optionId: resolution.options[0].optionId,
customer: {
name: "James Park",
email: "james@email.com",
},
},
{
idempotencyKey: "booking-james-aug05", // Safe to retry on network failure
}
);
// { id: "bkg_...", status: "pending" }
console.log(`Booking created: ${booking.id}`);
booking = client.confirm_resolution(
resolution_id=resolution["resolutionId"],
option_id=resolution["options"][0]["optionId"],
customer={"name": "James Park", "email": "james@email.com"},
idempotency_key="booking-james-aug05",
)
print(f"Booking created: {booking['id']}")
The Idempotency Key
Always pass an idempotencyKey. If your server retries the confirm call due to a network timeout, Orita will return the original booking response instead of creating a duplicate. The key must be unique per intent β a combination of user ID + slot + date works well.
If you call confirm twice with the same idempotency key but different parameters (e.g., a different optionId), Orita returns 409 Conflict. This protects against accidental mutation through retry.
Error Cases
410 β Resolution Expired
{ "error": "resolution_expired", "message": "Resolution res_... has expired." }
The 5-minute window passed. Re-run resolveScheduling and show fresh options to the user.
409 β Slot Already Taken
{ "error": "slot_unavailable", "message": "The selected slot is no longer available." }
Classic race condition. The recommended UI pattern:
try {
const booking = await orita.confirmResolution(resolutionId, payload, opts);
showConfirmationScreen(booking);
} catch (err) {
if (err.status === 409 && err.code === "slot_unavailable") {
// Slot was taken β fetch fresh options and re-present
const freshResolution = await orita.resolveScheduling(originalParams);
showOptionsScreen(freshResolution.options, {
notice: "That slot was just booked. Here are the next available options.",
});
}
}
409 β Idempotency Key Conflict
You reused a key with different parameters. Use a new, distinct key per booking intent.
410 β Hold Expired Before Confirm
If the hold TTL elapsed, the confirm will still attempt to book the slot β but if another session took it in the meantime, you'll get a slot_unavailable 409. Refresh and re-present.
Release the Hold If the User Declines
If the user looks at the options and decides not to book, release the hold immediately rather than waiting for TTL expiry. This keeps the slot available for other users.
await orita.releaseOption(resolution.resolutionId, resolution.options[0].optionId);
client.release_option(
resolution_id=resolution["resolutionId"],
option_id=resolution["options"][0]["optionId"],
)
Complete End-to-End Example
JavaScript / TypeScript (~40 lines)
import Orita from "orita-sdk";
const orita = new Orita({ apiKey: process.env.ORITA_API_KEY });
async function bookTherapySession(userId: string, userEmail: string, userName: string) {
// 1. Resolve options
const resolution = await orita.resolveScheduling({
service: "therapy",
constraints: {
languageCodes: { anyOf: ["es"] },
modalityCodes: { anyOf: ["virtual"] },
},
dateRange: { from: "2026-08-05", to: "2026-08-12" },
preferences: { dayParts: ["afternoon"] },
});
if (resolution.status === "zero_results" || resolution.options.length === 0) {
return { status: "no_availability" };
}
// 2. Present options to user (UI step β abbreviated here)
const chosenOption = resolution.options[0]; // user picked the top option
// 3. Hold the slot while user reads provider profile
const hold = await orita.holdOption(
resolution.resolutionId,
chosenOption.optionId,
120
);
console.log(`Slot held until ${hold.expiresAt}`);
// 4. User confirmed β book it
try {
const booking = await orita.confirmResolution(
resolution.resolutionId,
{
optionId: chosenOption.optionId,
customer: { name: userName, email: userEmail },
},
{ idempotencyKey: `booking-${userId}-${chosenOption.slotId}` }
);
return { status: "confirmed", bookingId: booking.id };
} catch (err) {
if (err.status === 409) {
await orita.releaseOption(resolution.resolutionId, chosenOption.optionId);
return { status: "slot_taken" };
}
throw err;
}
}
Python (~40 lines)
import os
import orita
client = orita.Client(api_key=os.environ["ORITA_API_KEY"])
def book_therapy_session(user_id: str, user_email: str, user_name: str) -> dict:
# 1. Resolve options
resolution = client.resolve_scheduling(
service="therapy",
constraints={
"languageCodes": {"anyOf": ["es"]},
"modalityCodes": {"anyOf": ["virtual"]},
},
date_range={"from": "2026-08-05", "to": "2026-08-12"},
preferences={"dayParts": ["afternoon"]},
)
if resolution["status"] == "zero_results" or not resolution["options"]:
return {"status": "no_availability"}
chosen = resolution["options"][0]
# 2. Hold the slot while user reviews provider
hold = client.hold_option(resolution["resolutionId"], chosen["optionId"], 120)
print(f"Slot held until {hold['expiresAt']}")
# 3. Confirm the booking
try:
booking = client.confirm_resolution(
resolution_id=resolution["resolutionId"],
option_id=chosen["optionId"],
customer={"name": user_name, "email": user_email},
idempotency_key=f"booking-{user_id}-{chosen['slotId']}",
)
return {"status": "confirmed", "booking_id": booking["id"]}
except orita.APIError as e:
if e.status == 409:
client.release_option(resolution["resolutionId"], chosen["optionId"])
return {"status": "slot_taken"}
raise
When to Skip the Hold
Holds add a network round-trip and introduce TTL management complexity. Skip them when:
- High provider supply, low contention β if you have 50+ providers and a given slot is rarely contested, the 409 path is the cheaper fallback.
- Immediate confirm β if your UX goes straight from "resolve" to "confirm" without a user-facing pause (e.g., a fully automated booking agent), skip hold entirely.
- Bulk scheduling β when scheduling multiple sessions in a loop, don't hold every option simultaneously. Confirm each one before moving to the next.
The hold is a courtesy mechanism for human-in-the-loop flows. Use it when the user pauses to read, decide, or compare profiles β not as a default in every path.
REST API Reference
If you're calling the API directly instead of using the SDK:
| Step | Method | Path |
|------|--------|------|
| Resolve | POST | /api/v1/resolutions |
| Fetch resolution | GET | /api/v1/resolutions/:id |
| Hold option | POST | /api/v1/resolutions/:id/options/:optionId/hold |
| Release hold | DELETE | /api/v1/resolutions/:id/options/:optionId/hold |
| Confirm | POST | /api/v1/resolutions/:id/confirm |
The POST /resolutions and POST /resolutions/:id/confirm endpoints both require an Idempotency-Key header and your platform API key in Authorization: Bearer <key>.
That's the complete flow. Resolve β (optionally) hold β confirm β handle errors. The mental model is simple once you see why the steps are separated. Next up: handling the booking.created webhook your confirm just fired β see Monitoring Webhook Deliveries.
