---
title: "Manejar errores y reintentos"
description: "Aplica las reglas de errores y reintentos de Arbol a tres flujos reales, un agendamiento que agotó el tiempo de espera, un conflicto de versión al actualizar y un 429."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.getarbol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Manejar errores y reintentos

[Respuestas y errores](/developers/concepts/responses-and-errors/) describe cómo se ve un fallo, y [Reintentos](/developers/concepts/retries/), 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:

```bash
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.json
```

Cuando 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`](/api/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:

1. **Espera.** No le ofrezcas otro horario al paciente ni le digas que falló: todavía no lo sabes.

2. **Busca la cita.** [`agenda.bookings.lookup`](/api/agenda/bookings/lookup/) es una lectura, así que puedes repetirla hasta que responda:

   ```bash
   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 `offeringId` del cupo que empiece en el `startsAt` del cupo y que no esté `cancelled`. Compara instantes, no texto: la misma hora se puede escribir con distintos desfases horarios.

3. **Si está, el agendamiento se hizo.** Léela con [`agenda.bookings.get`](/api/agenda/bookings/get/) para obtener sus indicaciones y su comprobante, y confírmasela al paciente.

4. **Si no está, envía de nuevo la misma solicitud**, con el mismo `slotToken`. `agenda.bookings.create` es idempotente: repetirla con la misma entrada no tiene ningún efecto adicional. Si ahora responde `INVALID_SLOT_TOKEN`, `RESOURCE_CAPACITY_EXCEEDED` o `SCHEDULE_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`](/api/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`](/api/agenda/bookings/cancel/) | Elimina; idempotente | Repítela, o lee la cita y comprueba que esté `cancelled`. |
| [`agenda.waitlist.join`](/api/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`](/api/raiz/contacts/update/), que necesita `raiz:contacts:write`, enviando la `version` que respondió [`raiz.contacts.get`](/api/raiz/contacts/get/):

```bash
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`:

```json
{
  "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:

1. **Vuelve a leer el registro** con `raiz.contacts.get`, para obtener su contenido actual y su `version`.
2. **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.
3. **Aplica tu cambio sobre lo que acabas de leer**, conservando los cambios de quien escribió antes, y envíalo con la nueva `version`.
4. **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](/developers/authentication/#manejo-seguro-de-las-api-keys). 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](/developers/sdks/typescript/):

### curl

```bash
# 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"
```

### TypeScript

```ts
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](#un-agendamiento-que-agotó-el-tiempo-de-espera).

Source: https://docs.getarbol.com/developers/guides/handle-errors-and-retries/index.mdx
