Flujo completo de una reserva

Inicio rápido muestra el camino feliz en cinco llamadas.
Esta página muestra el flujo entero que una integración real tiene que manejar:
qué se llama en cada paso, qué hay que guardar de cada respuesta y a qué paso
se vuelve cuando algo falla
.

La regla que ordena todo el flujo: nada queda apartado hasta que existe la
reserva.
La disponibilidad y la cotización son ciertas en el instante en que
responden. Entre eso y POST /bookings la habitación se puede vender, y la
integración tiene que estar preparada para volver atrás.

El mapa

flowchart TD
    A[GET /properties] --> B{api_rate_plans_configured?}
    B -- false --> X[El hotel debe crear un plan tarifario en el canal api]
    B -- true --> C[GET /units]
    C --> D[GET /availability]
    D --> E{available: true?}
    E -- no --> D2[Otras fechas u otra unidad] --> D
    E -- sí --> F[GET /rates]
    F --> G[POST /quote]
    G --> H{200?}
    H -- "422 RATE_PLAN_* / OCCUPANCY_*" --> F
    H -- sí --> I{¿Hace falta tiempo antes de comprometerse?}
    I -- no --> J[POST /bookings]
    I -- sí --> K[POST /bookings con hold_minutes]
    J -- "409 UNIT_NOT_AVAILABLE" --> D
    J -- 201 --> Z[Reserva pending: mostrar code]
    K -- "409 UNIT_NOT_AVAILABLE" --> D
    K -- "201 is_hold: true" --> L{¿El huésped se comprometió?}
    L -- sí --> M[POST /bookings/id/confirm]
    L -- no --> N[POST /bookings/id/cancel]
    L -- nadie responde --> O[El barrido libera el hold solo]
    M -- "409 BOOKING_HOLD_EXPIRED" --> D
    M -- 200 --> Z2[Reserva confirmed]

Antes de empezar

export AXIS_KEY="axis_live_…"
export AXIS="https://api.sandbox.axispro.travel/api/external/v1"

Los alcances que pide el flujo completo:

PasoLlamadaAlcance
1GET /propertiesproperties:read
2GET /units, GET /units/{unit}units:read
3GET /availabilityavailability:read
4–5GET /rates, POST /quoterates:read
6–7POST /bookings, POST /bookings/{booking}/confirmbookings:write
7POST /bookings/{booking}/cancelbookings:cancel
8GET /bookings, GET /bookings/{booking}bookings:read

Una key sin el alcance recibe 403 APIKEY_SCOPE_MISSING, y el mensaje nombra el
que falta. Si la key cubre varias propiedades, toda llamada desde el paso 2
lleva X-Property: <id de la propiedad> — ver
Alcances y el header X-Property.

1. La propiedad

curl "$AXIS/properties" -H "X-API-Key: $AXIS_KEY"
{
  "data": [
    {
      "id": "01a06051-69f0-72f8-8ad6-b46960495eb2",
      "name": "Hotel Casa Antigua",
      "timezone": "America/Guatemala",
      "default_currency": "USD",
      "check_in_time": "15:00:00",
      "check_out_time": "11:00:00",
      "api_rate_plans_configured": true
    }
  ]
}

Guardar: id (para X-Property si hace falta), timezone, check_in_time
y check_out_time para mostrarle al huésped.

Si api_rate_plans_configured es false, detenerse aquí. El hotel no tiene
ningún plan tarifario en el canal api, y todo lo que sigue va a volver vacío —
correctamente. Sólo el hotel lo puede arreglar; ver
Planes tarifarios.

2. El catálogo

curl "$AXIS/units" -H "X-API-Key: $AXIS_KEY"
{
  "data": [
    {
      "id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
      "name": "Deluxe Double",
      "is_room_type": false,
      "room_kind": "room",
      "max_adults": 4,
      "max_children": 2,
      "max_occupancy": 6,
      "size": null,
      "thumbnail_url": null
    }
  ],
  "meta": { "next_cursor": null, "per_page": 50 }
}

Este paso no cambia con cada búsqueda: el catálogo se puede cachear y refrescar
cada tanto. GET /units/{unit} trae el detalle completo — descripción, camas,
amenidades, imágenes — para una página de habitación. La lista se pagina por
cursor; ver Paginación.

Guardar: id, y max_adults / max_children para no ofrecer una ocupación
que la unidad no admite.

3. La disponibilidad

curl -G "$AXIS/availability" -H "X-API-Key: $AXIS_KEY" \
  -d start=2027-03-01 -d end=2027-03-04
{
  "data": [
    {
      "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
      "name": "Deluxe Double",
      "available": true,
      "remaining_capacity": 1
    }
  ],
  "meta": { "start_date": "2027-03-01", "end_date": "2027-03-04" }
}
  • start es la primera noche y end el día de salida: esto son 3 noches.
    La ventana máxima es de 365 días.
  • Vuelve una fila por unidad vendible. Una unidad sin plan en el canal api no
    aparece como available: false — no aparece.
  • remaining_capacity es 1 o 0 para una habitación física, y cuántas quedan
    para un Room Type.

Guardar: los unit_id con available: true.

4. Los precios

curl -G "$AXIS/rates" -H "X-API-Key: $AXIS_KEY" \
  -d from=2027-03-01 -d to=2027-03-03 \
  -d "unit_ids[]=01a06051-69f2-724a-bfe0-57b5b5dab318"
{
  "data": [
    {
      "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
      "name": "Deluxe Double",
      "rate_plans": [
        {
          "rate_plan_id": "01a06051-69f4-7072-b4da-9bb17cb00e73",
          "name": "Integrations Flexible",
          "days": [
            { "date": "2027-03-01", "price": "150.00", "currency": "USD", "min_stay": 1, "max_stay": null },
            { "date": "2027-03-02", "price": "150.00", "currency": "USD", "min_stay": 1, "max_stay": null },
            { "date": "2027-03-03", "price": "150.00", "currency": "USD", "min_stay": 1, "max_stay": null }
          ]
        }
      ]
    }
  ],
  "meta": { "from": "2027-03-01", "to": "2027-03-03" }
}

Las fechas no se escriben igual que en el paso 3. to es la última
noche
, incluida. Para la misma estadía del 1 al 4 de marzo, /availability
lleva end=2027-03-04 y /rates lleva to=2027-03-03.

  • Sólo aparecen planes del canal api. Una variación ("No reembolsable") llega
    como un plan más, con su propio rate_plan_id y su precio final — no hay
    descuento que aplicar del lado de la integración.
  • min_stay y max_stay son restricciones reales: una estadía que no las
    cumple se rechaza en los pasos 5 y 6.

Guardar: el rate_plan_id que elija el huésped, de los listados bajo esa
unidad
. Un plan de otra unidad responde RATE_PLAN_UNIT_NOT_LINKED.

5. La cotización

El cuerpo es exactamente el de la reserva, sin el huésped:

curl -X POST "$AXIS/quote" -H "X-API-Key: $AXIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
    "rate_plan_id": "01a06051-69f4-7072-b4da-9bb17cb00e73",
    "start_date": "2027-03-01",
    "end_date": "2027-03-04",
    "adults": 2
  }'
{
  "data": {
    "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
    "rate_plan_id": "01a06051-69f4-7072-b4da-9bb17cb00e73",
    "start_date": "2027-03-01",
    "end_date": "2027-03-04",
    "nights": 3,
    "quantity": 1,
    "currency": "USD",
    "subtotal": "450.00",
    "total_price": "450.00",
    "total_with_tax": "450.00",
    "tax_amount": "0.00",
    "nightly": [
      { "date": "2027-03-01", "amount": "150.00" },
      { "date": "2027-03-02", "amount": "150.00" },
      { "date": "2027-03-03", "amount": "150.00" }
    ],
    "fees": [],
    "available": true
  }
}

La cotización corre las mismas validaciones que la reserva — plan enganchado a
la unidad, estadía mínima y máxima, ocupación. Un 200 aquí es una estadía que
POST /bookings va a aceptar, mientras la habitación siga libre.
available
es informativo y cierto sólo en este instante.

Mostrar: total_with_tax, que es lo que debe el huésped. El dinero llega
como string con dos decimales; parsearlo como float pierde centavos en silencio.

Si respondeQué pasóVolver a
404 UNIT_NOT_FOUNDLa unidad dejó de ser vendible por la APIPaso 2
404 RATE_PLAN_NOT_FOUNDEl plan no está activo en el canal apiPaso 4
422 RATE_PLAN_UNIT_NOT_LINKEDEl plan no pertenece a esa unidadPaso 4
422 RATE_PLAN_MIN_STAY / RATE_PLAN_MAX_STAYLa estadía no cumple el plan; trae min_stay o max_stay y nightsPaso 3, con otras fechas
422 OCCUPANCY_OUT_OF_BOUNDSMás huéspedes de los que admite la unidadPaso 2
422 RATE_PLAN_OCCUPANCY_NOT_CONFIGUREDEl plan no tiene precio para esa cantidad de adultos; trae requested_adults y configured_adultsPaso 4, otro plan

6. La reserva

Aquí el flujo se abre en dos, y la pregunta que decide es: ¿pasa algo entre
que el huésped elige y que se compromete?
Una tarjeta que se autoriza, una
segunda pantalla, una aprobación de la empresa.

  • No → reserva directa (6a).
  • Sí → reserva con hold (6b), y después el paso 7.

6a. Reserva directa

curl -X POST "$AXIS/bookings" -H "X-API-Key: $AXIS_KEY" \
  -H "Idempotency-Key: carrito-8842" \
  -H "Content-Type: application/json" \
  -d '{
    "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
    "rate_plan_id": "01a06051-69f4-7072-b4da-9bb17cb00e73",
    "start_date": "2027-03-01",
    "end_date": "2027-03-04",
    "adults": 2,
    "guest": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]",
      "phone": "+34600000000"
    },
    "notes": "Llegada tarde, cerca de las 23:00."
  }'

201 Created:

{
  "data": {
    "id": "01a06052-324e-72d6-8bbe-7f6fe7dd29f2",
    "code": "P8963-000001",
    "status": "pending",
    "is_hold": false,
    "hold_expires_at": null,
    "start_date": "2027-03-01",
    "end_date": "2027-03-04",
    "nights": 3,
    "adults": 2,
    "children": 0,
    "infants": 0,
    "currency": "USD",
    "total_price": "450.00",
    "paid_amount": "0.00",
    "balance_due": "450.00",
    "unit": { "id": "01a06052-3219-71e2-9537-1b6232c6d145", "name": "Deluxe Double" },
    "guest": {
      "id": "01a06052-324c-7326-81b9-ea40f3f80f71",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "[email protected]",
      "phone": "+34600000000"
    },
    "created_at": "2026-09-02T04:13:14+00:00"
  }
}

Guardar: id (para confirmar, cancelar o releer) y code, que es la
referencia que usan el huésped y el hotel — hay que mostrársela al huésped.

Lo que conviene saber de esta respuesta:

  • Idempotency-Key es obligatorio. Se deriva de algo estable del sistema
    propio — un carrito, una orden — y se reusa en cada reintento del mismo
    intento. Ver Idempotencia.
  • La reserva nace pending y sin pagar. Esta API no toma pagos.
  • El total que cuenta es el de la reserva, no el de la cotización.
  • Leer balance_due; no calcular total_price − paid_amount. Ver
    Reservas.
  • guest siempre crea un huésped nuevo, aunque el email ya exista. Para
    reusar uno, mandar guest_id en su lugar. Uno de los dos es obligatorio.
  • Una habitación por llamada. Cinco habitaciones son cinco llamadas, cada
    una con su propia Idempotency-Key.

6b. Reserva con hold

El mismo cuerpo, más hold_minutes (de 1 a 30):

curl -X POST "$AXIS/bookings" -H "X-API-Key: $AXIS_KEY" \
  -H "Idempotency-Key: carrito-8842" \
  -H "Content-Type: application/json" \
  -d '{
    "unit_id": "01a06051-69f2-724a-bfe0-57b5b5dab318",
    "rate_plan_id": "01a06051-69f4-7072-b4da-9bb17cb00e73",
    "start_date": "2027-03-01",
    "end_date": "2027-03-04",
    "adults": 2,
    "hold_minutes": 15,
    "guest": { "first_name": "Ada", "last_name": "Lovelace", "email": "[email protected]" }
  }'
{
  "data": {
    "id": "01a06052-324e-72d6-8bbe-7f6fe7dd29f2",
    "code": "P8963-000001",
    "status": "pending",
    "is_hold": true,
    "hold_expires_at": "2027-02-10T14:15:00+00:00"
  }
}

La habitación queda fuera de la venta desde este momento, pero la reserva no
aparece en las listas del hotel hasta que se confirma. Un hold aparta la
habitación, no congela el precio. Ver Holds.

Si el paso 6 falla

Si respondeQué pasóQué hacer
409 UNIT_NOT_AVAILABLELa habitación se vendió después de cotizar. En un Room Type puede traer remaining y requestedVolver al paso 3
409 IDEMPOTENCY_IN_FLIGHTUn reintento propio llegó antes de que terminara el primeroEsperar y reintentar con la misma key
409 IDEMPOTENCY_KEY_REUSEDLa misma key con otro cuerpoError de la integración: reserva nueva, key nueva
422 IDEMPOTENCY_KEY_REQUIREDFalta el headerMandarlo
422 RATE_PLAN_* · OCCUPANCY_OUT_OF_BOUNDSIgual que en el paso 5Ver la tabla del paso 5
404 GUEST_NOT_FOUNDEl guest_id no existe o fue borradoMandar un objeto guest
429 KEY_BOOKING_QUOTA_EXCEEDEDLa key llegó a su tope diario de reservas; trae limit, used y Retry-AfterEsperar, o pedirle al hotel que lo suba
429 KEY_HOLD_LIMIT_REACHEDSólo con hold: la key ya tiene su máximo de holds vivosCerrar holds abiertos (paso 7)
Timeout, sin respuestaNo se sabe si la reserva se creóReintentar con la misma key y el mismo cuerpo

Un timeout no es una falla. Con la misma Idempotency-Key y el mismo cuerpo,
si el primer intento creó la reserva vuelve ese mismo 201; si no, este la crea.
Un intento que falló con 4xx o 5xx libera la key, así que se puede corregir el
cuerpo y reintentar con ella.

7. Cerrar el hold

Sólo para el camino 6b. Hay tres salidas.

El huésped se comprometió → confirmar:

curl -X POST "$AXIS/bookings/01a06052-324e-72d6-8bbe-7f6fe7dd29f2/confirm" \
  -H "X-API-Key: $AXIS_KEY"

200, con la reserva en status: "confirmed", is_hold: false y
hold_expires_at: null.

El huésped abandonó → cancelar:

curl -X POST "$AXIS/bookings/01a06052-324e-72d6-8bbe-7f6fe7dd29f2/cancel" \
  -H "X-API-Key: $AXIS_KEY"

200, con la reserva en status: "cancelled". La habitación vuelve a la venta
de inmediato y el cupo de holds se libera. Requiere bookings:cancel.

Nadie hace nada → se libera solo. Un barrido cada cinco minutos cancela los
holds vencidos. Funciona, pero el cupo de holds de la key queda ocupado hasta
entonces; cerrar explícitamente es mejor.

Ninguna de las dos llamadas lleva Idempotency-Key: repetir un confirm o un
cancel devuelve el mismo 200.

Si respondeQué pasóQué hacer
409 BOOKING_HOLD_EXPIREDEl reloj venció antes del confirm; la habitación pudo venderseVolver al paso 3
422 BOOKING_NOT_CONFIRMABLELa reserva ya no está pendingReleerla (paso 8)
422 BOOKING_NOT_CANCELLABLELa estadía ya empezó o terminóEs decisión del hotel

cancel también sirve para una reserva directa (6a) mientras siga pending o
confirmed.

8. Releer la reserva

curl "$AXIS/bookings/01a06052-324e-72d6-8bbe-7f6fe7dd29f2" -H "X-API-Key: $AXIS_KEY"

Devuelve la misma forma que el paso 6. GET /bookings lista las reservas, con
filtros from, to y status, paginadas por cursor.

Una key sólo ve las reservas que ella misma creó. Una reserva de otra key
responde 404 BOOKING_NOT_FOUND, igual que una que no existe. Para ver todas las
de la propiedad hace falta además bookings:read:all, que el hotel otorga por
separado.

Resumen

  1. GET /properties → seguir sólo si api_rate_plans_configured es true.
  2. GET /units → catálogo, cacheable.
  3. GET /availability → quedarse con los unit_id disponibles.
  4. GET /rates → elegir un rate_plan_id de esa unidad, respetando
    min_stay y max_stay.
  5. POST /quote → mostrar total_with_tax.
  6. POST /bookings con una Idempotency-Key estable → mostrar code.
    Con hold_minutes si hace falta tiempo antes de comprometerse.
  7. Con hold: confirm o cancel, sin dejarlo vencer.
  8. Ante 409 UNIT_NOT_AVAILABLE o 409 BOOKING_HOLD_EXPIRED, volver al
    paso 3. Ante un timeout, repetir el paso 6 con la misma key.

Relacionado

  • Inicio rápido — el camino feliz, en cinco llamadas.
  • Idempotencia — antes de poner en producción algo que
    reintente.
  • Holds — el reloj, el barrido y el techo de holds vivos.
  • Errores — todos los códigos, y los 422 que no traen
    error_code.
  • Límites de uso — el límite por minuto y los dos techos
    de inventario.

Did this page help you?