
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 | ParĂĄmetros invĂĄlidos, dateRange mal formateado, campo requerido faltante. |
| 401 | Unauthorized | API key invĂĄlida, expirada, o faltante. |
| 403 | Forbidden | API key vĂĄlida pero sin permiso para este recurso (provider no vinculado a tu plataforma). |
| 404 | Not Found | Recurso no existe (resolution, booking, provider). |
| 409 | Conflict | Idempotency key reusada con payload diferente, slot ya tomado por otro booking. |
| 410 | Gone | Resolution expirada (TTL 5 minutos), hold expirado (10 minutos). |
| 429 | Too Many Requests | Rate limit excedido. Reintentar después del tiempo en el header Retry-After. |
| 500 | Internal Server Error | Error inesperado. Reintentar con backoff exponencial. |
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."
}
]
}NingĂșn provider habla el idioma requerido.
Suggestion: Añade el atributo de idioma a mås providers o relaja este constraint.
NingĂșn provider ofrece la modalidad solicitada (virtual/in-person).
Suggestion: Verifica que algĂșn provider tenga configurada esa modalidad.
NingĂșn provider tiene esa especialidad.
Suggestion: Revisa el slug de especialidad o añade mås providers con esa especialidad.
NingĂșn provider acepta ese seguro.
Suggestion: Verifica el slug del seguro o amplĂa la red de providers.
Hay providers elegibles pero sin slots en el dateRange.
Suggestion: AmplĂa el dateRange o pide al provider que actualice su disponibilidad.
NingĂșn provider tiene ese service/event-type.
Suggestion: Crea el servicio en el provider correspondiente antes de resolver.
Rango muy corto, no hay slots disponibles.
Suggestion: AmplĂa el rango de fechas al menos a 3â7 dĂas.
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),
});