
Complete guide to HTTP error codes, zero-result reason codes, retry strategies, and hold expiry handling for the Orita scheduling API.
All API responses use standard HTTP status codes. The table below lists every code you may encounter and when to expect it.
| Status | Name | When it occurs |
|---|---|---|
| 400 | Bad Request | Invalid parameters, malformed dateRange, or missing required field. |
| 401 | Unauthorized | API key is invalid, expired, or missing. |
| 403 | Forbidden | Valid API key but insufficient permission for this resource (provider not linked to your platform). |
| 404 | Not Found | Requested resource does not exist (resolution, booking, or provider). |
| 409 | Conflict | Idempotency key reused with a different payload, or slot already taken by another booking. |
| 410 | Gone | Resolution expired (TTL 5 minutes) or hold expired (max 10 minutes). |
| 429 | Too Many Requests | Rate limit exceeded. Retry after the number of seconds in the Retry-After header. |
| 500 | Internal Server Error | Unexpected server error. Retry with exponential backoff or contact support. |
All error responses share the same JSON structure:
{
"error": {
"code": "LICENSE_REGION_MISMATCH",
"message": "No provider met the required license-region rule.",
"status": 422,
"requestId": "req_01K...",
"field": "constraints.licenseRegionCodes",
"recoverable": true,
"suggestedAction": "Remove or broaden the required license region."
}
}Every v2 response includes X-Orita-Request-Id for tracing. The code is stable for programmatic matching. The recoverable field tells you whether retrying makes sense.
When POST /api/v2/resolutions finds no provider matching the constraints, it returns status: "zero_results" (HTTP 200) with an explanation:
{
"resolutionId": "res_01JZBK...",
"status": "zero_results",
"options": [],
"totalProviders": 20,
"evaluatedSlots": 0,
"zeroResultReasons": [
{
"reasonCode": "NO_LANGUAGE_MATCH",
"description": "No providers in your network speak the requested language.",
"affectedProviders": 20,
"suggestion": "Add the language attribute to more providers, or relax this constraint."
}
]
}No provider in your network speaks the required language.
Suggestion: Add the language attribute to more providers, or relax this constraint.
No provider offers the requested modality (virtual or in-person).
Suggestion: Verify that at least one provider has the modality configured.
No provider has the requested specialty.
Suggestion: Check the specialty slug or add more providers with that specialty.
No provider accepts the requested insurance plan.
Suggestion: Verify the insurance plan slug or expand your provider network.
Eligible providers exist but have no available slots in the requested date range.
Suggestion: Expand the dateRange or ask the provider to update their availability schedule.
No provider has the requested service or event type.
Suggestion: Create the service on the relevant provider before running a resolution.
Date range is too narrow to find available slots.
Suggestion: Expand the date range to at least 3–7 days.
For 429 and 5xx responses, use exponential backoff. Never retry 4xx errors (except 429) — they indicate a problem with the request itself.
// Exponential backoff example
async function resolveWithRetry(payload, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const res = await fetch('/api/v2/resolutions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ORITA_API_KEY}`,
'Idempotency-Key': `${payload.sessionId}-${attempt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5');
await sleep(retryAfter * 1000);
continue;
}
if (res.status >= 500) {
await sleep(Math.pow(2, attempt) * 1000); // 1s, 2s, 4s
continue;
}
return await res.json();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Holds expire after 10 minutes. Resolutions expire after 5 minutes. Both return 410 Gone when accessed after expiry.
A 410 Gone on a hold does not mean the booking failed — it means the hold window elapsed. You must re-resolve to find a new available slot.
const holdRes = await orita.resolutions.hold(optionId);
// If hold expires before confirmation:
try {
await orita.resolutions.confirm(holdId);
} catch (err) {
if (err.status === 410) {
// Re-resolve: the slot may have been taken
const newResolution = await orita.resolutions.resolve({ ... });
// Show new options to user
}
}All POST /api/v2/resolutions calls require an Idempotency-Key header.
✓ Same key + same payload
Returns the cached response. Safe to retry after a network failure.
✗ Same key + different payload
Returns 409 Conflict. Generate a new key for a different request.
Best practice: derive the key from your session ID + a hash of the request payload.
import { createHash } from 'crypto';
function idempotencyKey(sessionId: string, payload: object): string {
const hash = createHash('sha256')
.update(JSON.stringify(payload))
.digest('hex')
.slice(0, 12);
return `${sessionId}-${hash}`;
}
// Usage
const key = idempotencyKey(session.id, resolutionPayload);
await fetch('/api/v2/resolutions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ORITA_API_KEY}`,
'Idempotency-Key': key,
'Content-Type': 'application/json',
},
body: JSON.stringify(resolutionPayload),
});