Respuestas y errores describe cómo se ve un fallo, y Reintentos, cuándo es seguro repetir una llamada. Esta guía aplica ambas páginas a tres situaciones que toda integración encuentra: un agendamiento que agotó el tiempo de espera, un conflicto de versión al actualizar y un 429. Lee primero esas dos páginas; esta no las repite. Los ejemplos usan curl y jq, con una API key en ARBOL_API_KEY, y todos los ids y datos de pacientes que aparecen son de ejemplo.
Conservar lo que dice cada respuesta
Cada decisión de abajo se basa en el estado, error.code, error.retryable y, cuando viene, el encabezado Retry-After. Guarda por separado los encabezados y el cuerpo de cada respuesta, y registra metadata.requestId con cada fallo:
status=$(curl --silent --show-error --max-time 30 \
--dump-header headers.txt --output body.json --write-out "%{http_code}" \
https://api.getarbol.com/v1/agenda/booking-requirements \
--header "Authorization: Bearer $ARBOL_API_KEY")
jq --arg status "$status" '{$status, code: .error.code, retryable: .error.retryable, requestId: .metadata.requestId}' body.jsonCuando no llega ninguna respuesta, curl termina con un error, como 28 cuando se agota --max-time, y el estado es 000. En una escritura, ese es un resultado que todavía no conoces.
Un agendamiento que agotó el tiempo de espera
Enviaste agenda.bookings.create y no recibiste respuesta, o recibiste un 504 UPSTREAM_TIMEOUT o un 502 OUTCOME_UNKNOWN. Diga lo que diga retryable, puede que la cita exista, y las operaciones de agendamiento no reciben clave de idempotencia. Averígualo antes de hacer cualquier otra cosa:
-
Espera. No le ofrezcas otro horario al paciente ni le digas que falló: todavía no lo sabes.
-
Busca la cita.
agenda.bookings.lookupes una lectura, así que puedes repetirla hasta que responda:curl https://api.getarbol.com/v1/agenda/bookings/lookup \ --request POST \ --header "Content-Type: application/json" \ --data '{ "documentNumber": "1234567890", "documentType": "CC" }' \ --header "Authorization: Bearer $ARBOL_API_KEY" \ | jq '.data.appointments[] | {id, startsAt, offeringId, status}'Busca una cita del
offeringIddel cupo que empiece en elstartsAtdel cupo y que no estécancelled. Compara instantes, no texto: la misma hora se puede escribir con distintos desfases horarios. -
Si está, el agendamiento se hizo. Léela con
agenda.bookings.getpara obtener sus indicaciones y su comprobante, y confírmasela al paciente. -
Si no está, envía de nuevo la misma solicitud, con el mismo
slotToken.agenda.bookings.createes idempotente: repetirla con la misma entrada no tiene ningún efecto adicional. Si ahora respondeINVALID_SLOT_TOKEN,RESOURCE_CAPACITY_EXCEEDEDoSCHEDULE_CHANGED, el horario ya no está: busca otro cupo y vuelve a preguntarle al paciente.
Las demás escrituras de agendamiento necesitan el mismo cuidado, cada una según su efecto:
| Operación | Efecto | Cuando su resultado es desconocido |
|---|---|---|
agenda.bookings.reschedule |
Escribe; no idempotente | Lee la cita con agenda.bookings.get. Si ya empieza en el horario nuevo, se movió; repite solo si no se movió. |
agenda.bookings.cancel |
Elimina; idempotente | Repítela, o lee la cita y comprueba que esté cancelled. |
agenda.waitlist.join |
Escribe; idempotente | Repítela: el mismo paciente, servicio y motivo devuelven la misma entrada abierta. |
Un conflicto de versión al actualizar
Una actualización que recibe expectedVersion se aplica solo si nadie cambió el registro desde que lo leíste. Aquí, un proceso de sincronización escribe el summary de un contacto de Raíz con raiz.contacts.update, que necesita raiz:contacts:write, enviando la version que respondió raiz.contacts.get:
curl https://api.getarbol.com/v1/raiz/contacts/cnt_00000000-0000-4000-8000-000000000009 \
--request PATCH \
--header "Authorization: Bearer $ARBOL_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"expectedVersion": 7,
"summary": "Prefiere que lo contacten en la mañana."
}'Alguien cambió el contacto entre tanto, así que responde 409 con este error:
{
"code": "VERSION_CONFLICT",
"message": "The resource changed after it was read. Read it again and retry with the new version.",
"retryable": false,
"data": {}
}retryable es false: la misma solicitud fallaría de la misma forma. En lugar de repetirla:
- Vuelve a leer el registro con
raiz.contacts.get, para obtener su contenido actual y suversion. - Revisa si tu cambio ya está. Repetir una actualización que ya se aplicó, como un intento tuyo anterior que agotó el tiempo de espera, también responde
VERSION_CONFLICT. Si el cambio ya está, terminaste. - Aplica tu cambio sobre lo que acabas de leer, conservando los cambios de quien escribió antes, y envíalo con la nueva
version. - Detente después de unos pocos conflictos seguidos y repórtalos con sus ids de solicitud: algo más sigue cambiando el registro.
Las actualizaciones de Agenda que reciben una versión, las de servicios, ofertas, recursos, reglas de horario y citas, aceptan solo personas, así que vienen de herramientas del personal. Responden 409 con un código que nombra el registro, y cada uno se maneja de la misma forma:
error.code |
Mensaje |
|---|---|
SERVICE_VERSION_MISMATCH |
The service changed after it was loaded. |
OFFERING_VERSION_MISMATCH |
The offering changed after it was loaded. |
RESOURCE_VERSION_MISMATCH |
The resource changed after it was loaded. |
SCHEDULE_RULE_VERSION_MISMATCH |
The schedule rule changed after it was loaded. |
SCHEDULE_RULES_CHANGED |
The schedule changed after it was loaded. |
APPOINTMENT_VERSION_MISMATCH |
The appointment changed after it was loaded. |
Un 429
429 RATE_LIMITED es retryable: espera los segundos de Retry-After y luego envía de nuevo la misma solicitud. El límite cuenta las solicitudes de una credencial, una razón más para darle a cada sistema su propia key. Envuelve tus llamadas para que todos los flujos esperen de la misma forma; en TypeScript, arbol es el cliente de createClient, como en el SDK de TypeScript:
# Envía una solicitud; mientras la respuesta sea 429, espera los segundos de Retry-After y la envía de nuevo.
arbol_request() {
local attempt status delay
for attempt in 1 2 3 4 5; do
status=$(curl --silent --show-error --max-time 30 \
--dump-header headers.txt --output body.json --write-out "%{http_code}" \
--header "Authorization: Bearer $ARBOL_API_KEY" "$@") || return 1
[ "$status" = 429 ] || break
delay=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' headers.txt)
sleep "${delay:-$((2 ** attempt))}"
done
cat body.json
}
arbol_request --get https://api.getarbol.com/v1/agenda/nearest-booking-slots \
--data-urlencode "serviceId=svc_00000000-0000-4000-8000-000000000001" \
--data-urlencode "timezone=America/Bogota"import { ApiError } from "@arbol-ai/sdk";
// Llama a la API; mientras la respuesta sea 429, espera los segundos de Retry-After y vuelve a llamar.
async function withRetryAfter<T>(call: () => Promise<T>, attempts = 5): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await call();
} catch (error) {
if (!(error instanceof ApiError) || error.status !== 429 || attempt === attempts) throw error;
const seconds = Number(error.response.headers.get("retry-after")) || 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
}
const { data } = await withRetryAfter(() =>
arbol.agenda.nearestBookingSlots.list({ serviceId: "svc_00000000-0000-4000-8000-000000000001", timezone: "America/Bogota" }),
);Ambos se detienen después de cinco intentos y se rinden de inmediato cuando no llega ninguna respuesta: en una escritura, ese es el resultado desconocido de arriba, y esperar no lo resuelve. Repite un 503 o un 504 de la misma forma solo en lecturas, con backoff exponencial y jitter; una escritura que terminó en cualquiera de los dos sigue los pasos de un agendamiento que agotó el tiempo de espera.