Documentación de APIs / Customers / v2 / Guía de integración

API Customers E-VTC v2 — Guía de integración para agencias y clientes

Versión del documento: 1.1 (septiembre 2026) — cambios respecto a la 1.0 en la sección 15 Audiencia: equipos de desarrollo de agencias/clientes que integran su aplicación con la plataforma para tarificar, crear y gestionar sus propias reservas. Base URL: https://<dominio-que-se-os-facilite>/api-ext/v2

Entorno de pruebas (sandbox): se os facilitarán credenciales y URL propias. El desarrollo y las pruebas se hacen siempre contra la sandbox; las credenciales de producción nunca son las mismas que las de pruebas.


1. Visión general

El modelo es de agencia: vuestra aplicación crea reservas en la plataforma en nombre de uno o varios clientes, y las consulta, edita o cancela después. La plataforma nunca llama a vuestros servidores.

Hay cuatro tipos de reserva (bookingType):

bookingType Qué es Precio
transfer Traslado por zona: de un origen a un destino Depende de las zonas de origen/destino
disposal Disposición: vehículo con conductor contratado por horas Depende del tipo de servicio y las horas contratadas
distanceTransfer Traslado por distancia: de un origen a un destino, tarifado por kilómetros Depende del tipo de servicio y los metros recorridos
experience Experiencia: un traslado por zona más una experiencia con sus actividades Tarifa de la experiencia por pasajero (adulto/niño) o por servicio

Los cuatro tipos se tarifican, se crean, se leen, se editan y se cancelan por los mismos endpoints; lo que cambia entre ellos es el bloque propio del payload (disposal, distance, experience) y qué endpoint de precio se consulta antes.

Flujo típico:

   GET  /customers/clients                    ← vuestros clientes y sus códigos
   GET  /customers/zones                      ← zonas de cobertura
                 │
                 ▼
   POST /customers/search-results             ← tarificar un traslado por zona
   POST /customers/search-results/disposal    ← tarificar una disposición
   POST /customers/search-results/distance    ← tarificar un traslado por distancia
   POST /customers/experiences                ← experiencias vendibles con precio
   POST /customers/extras/by-service-type     ← extras disponibles
                 │
                 ▼
   POST /customers/bookings                   ← crear la reserva
                 │
                 ▼
   GET  /customers/bookings[?bookingType=…]   ← seguimiento (estado, conductor, vehículo)
   PUT  /customers/bookings/{id}              ← editar (dentro de la ventana de edición)
   DELETE /customers/bookings/{id}            ← cancelar

2. Autenticación

OAuth2 client credentials. Se os entregan un client_id y un client_secret.

Obtener token

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -X POST "https://<dominio>/api-ext/v2/oauth/token?grant_type=client_credentials"

⚠️ Las credenciales van en el header Authorization: Basic (como hace curl -u). Enviarlas en el body devuelve 401 "Invalid Authorization header".

Respuesta:

{ "access_token": "eyJ0eXAi...", "token_type": "Bearer", "expires_in": 900 }

Reglas


3. Clientes y clientCode

Una misma credencial puede operar en nombre de uno o varios clientes de la plataforma.

GET /customers/clients
{ "clients": [ { "code": "AGEN01", "name": "Agencia Ejemplo", "taxId": "B12345678" } ] }

Regla del campo clientCode en el resto de operaciones (tarificación, extras, creación):

Credencial con… clientCode
Un cliente Opcional. Si se envía y no coincide → 403
Varios clientes Obligatorio (400 si falta, 403 si no os pertenece)

En la edición no se usa: el cliente es el de la reserva ya creada.

En el listado es un filtro opcional: GET /customers/bookings?clientCode=AGEN01 devuelve solo las reservas de ese cliente (403 si el código no os pertenece). Sin él siguen llegando las de todos los clientes de la credencial, que es el comportamiento por defecto.

Y no hace falta filtrar para saber de quién es cada reserva: tanto el listado como el detalle devuelven el bloque client con el code y el name del cliente al que pertenece.

"client": { "code": "AGEN01", "name": "Agencia Ejemplo" }

client.code es el mismo valor que clientCode en el alta y que code en /customers/clients — cruzad por ahí si necesitáis el taxId u otros datos del cliente.

En el detalle (GET /customers/bookings/{bookingReference}) clientCode no se usa: si se envía, se ignora en silencio. La reserva ya viene con su client, y el control de acceso es el mismo (404 si el id es de un cliente que no os pertenece).


4. Zonas

Las zonas determinan la cobertura de los cuatro tipos de reserva y el precio de los traslados por zona y las experiencias. Al crear cualquier reserva, el origen y el destino deben caer en una zona (o llevar un zoneCode válido): si no, 404 "Locations not found.". En disposiciones y traslados por distancia la zona no interviene en el precio, pero sí en la cobertura y en la operativa (informes, asignación a colaboradores).

GET /customers/zones                                   → { "zones": [ …todas… ] }
GET /customers/zones/{code}                            → { "zone": { … } }        (404 si no existe)
GET /customers/zones/by-coordinates?latitude=…&longitude=…  → { "zone": { … } }   (404 si ninguna zona las contiene)

Objeto zona:

{
  "code": "ALC",
  "name": "Aeropuerto de Alicante",
  "type": "AIRPORT",
  "airportCode": "ALC",
  "area": { "...": "geometría (polígono / círculo / rectángulo / multiárea)" }
}

type es AIRPORT o STREET_ADDRESS. airportCode solo aparece en aeropuertos.


5. Tarificar un traslado por zona

POST /customers/search-results
{
  "clientCode": "AGEN01",
  "origin":      { "latitude": 38.2822, "longitude": -0.5581 },
  "destination": { "latitude": 38.5382, "longitude": -0.1300 },
  "passengers": 4,
  "pickupDateTime": "2026-09-01T10:00:00Z",
  "pickupTimezone": "Europe/Madrid"
}

Todos los campos son obligatorios (salvo clientCode, sección 3). pickupDateTime con sufijo Z; pickupTimezone una zona IANA válida — se valida pero no tiene efecto sobre el precio (igual que en la disposición). Sobre cómo se interpreta la hora, leed la sección 8.1: es la misma regla que en la creación.

Respuesta — un resultado por tipo de servicio con precio configurado:

{
  "results": [
    {
      "id": 9,
      "code": "001",
      "transportCategory": "Sedan",
      "transportDescription": "Hasta 4 pasajeros",
      "transportImage": "https://…",
      "price": {
        "salePrice": 45.0,
        "priceFormattedWithSymbol": "45,00 €",
        "currency": "EUR",
        "vatPercentage": 10,
        "salePriceWithVat": 49.5,
        "salePriceWithVatFormatted": "49,50 €"
      },
      "maxPassengers": 4,
      "maxSuitcases": 4,
      "features": [ { "name": "Silla de bebé", "value": 2 } ]
    }
  ]
}

6. Tarificar una disposición

POST /customers/search-results/disposal

El tipo de reserva va en la URL, no en el cuerpo: cada tarificación tiene sus propios campos obligatorios.

{
  "clientCode": "AGEN01",
  "serviceType": "001",
  "disposal": { "contractedHours": 6 },
  "pickupDateTime": "2026-09-01T10:00:00Z"
}
Campo Reglas
serviceType Obligatorio. Código del tipo de servicio (400 si no existe)
disposal.contractedHours Obligatorio. Entero ≥ 1
pickupDateTime Obligatorio. UTC estricto
pickupTimezone Opcional y sin efecto (se admite por simetría; el precio depende del instante UTC)
Origen / destino No se piden: el precio no depende de las zonas

Respuesta:

{
  "serviceType": { "id": 9, "code": "001", "transportCategory": "Sedan" },
  "disposal": {
    "contractedHours": 6,
    "packageHours": 4,
    "packagePrice": 80.0,
    "extraHours": 2,
    "extraHourPrice": 10.0,
    "includedKm": 120.0,
    "extraKmPrice": 0.5
  },
  "price": {
    "salePrice": 100.0,
    "priceFormattedWithSymbol": "100,00 €",
    "currency": "EUR",
    "vatPercentage": 10,
    "salePriceWithVat": 110.0,
    "salePriceWithVatFormatted": "110,00 €"
  }
}

Sobre el desglose disposal:


6.1. Tarificar un traslado por distancia

POST /customers/search-results/distance
{
  "clientCode": "AGEN01",
  "serviceType": "002",
  "origin":      { "latitude": 36.6749, "longitude": -4.4991 },
  "destination": { "latitude": 36.4848, "longitude": -4.9530 },
  "distance": { "meters": 48200, "durationSeconds": 2700 },
  "usesHighway": true,
  "pickupDateTime": "2026-09-20T07:00:00Z"
}
Campo Reglas
serviceType Obligatorio. Código del tipo de servicio (400 si no existe)
origin / destination Obligatorios, solo coordenadas
distance.meters / distance.durationSeconds Opcionales aquí (obligatorios en el alta). Enteros ≥ 1. Van juntos o ninguno (400 si solo llega uno). Si no se envían, la plataforma calcula distancia y duración a partir de las coordenadas
usesHighway Opcional. Si la ruta calculada debe ir por autopista
pickupDateTime Obligatorio. UTC estricto
pickupTimezone Opcional y sin efecto

Respuesta:

{
  "serviceType": { "id": 2, "code": "002", "transportCategory": "Van" },
  "distance": { "meters": 48200, "durationSeconds": 2700 },
  "usesHighway": true,
  "price": {
    "salePrice": 96.4,
    "priceFormattedWithSymbol": "96,40 €",
    "currency": "EUR",
    "vatPercentage": 10,
    "salePriceWithVat": 106.04,
    "salePriceWithVatFormatted": "106,04 €"
  }
}

6.2. Experiencias vendibles con precio

POST /customers/experiences
{
  "clientCode": "AGEN01",
  "pickupDateTime": "2026-10-02T08:00:00Z",
  "numberOfAdults": 2,
  "numberOfChildren": 2
}

pickupDateTime, numberOfAdults y numberOfChildren son obligatorios (los pasajeros, enteros ≥ 0). Devuelve las experiencias que el cliente puede vender en esa fecha, con el importe total ya calculado para esos pasajeros:

{
  "experiences": [
    {
      "id": 17,
      "name": "Ruta de bodegas",
      "pricePerPassenger": true,
      "activities": [ "Visita a bodega", "Cata", "Almuerzo" ],
      "adultPriceWithoutVat": 35.0,
      "childPriceWithoutVat": 24.0,
      "price": {
        "salePrice": 118.0,
        "priceFormattedWithSymbol": "118,00 €",
        "currency": "EUR",
        "vatPercentage": 0,
        "salePriceWithVat": 118.0,
        "salePriceWithVatFormatted": "118,00 €"
      }
    }
  ]
}

7. Extras

POST /customers/extras/by-service-type
{ "serviceType": "001", "clientCode": "AGEN01" }

serviceType es el código del tipo de servicio: el code de la tarificación y el mismo valor que devuelve la lectura de una reserva. 404 "Not found." si no existe.

{
  "extras": [
    {
      "id": 3,
      "name": "Silla de bebé",
      "description": "Grupo 0-1",
      "icon": "<svg …>",
      "price": {
        "amount": 6.05,
        "amountFormatted": "6,05 €",
        "amountWithoutVat": 5.5,
        "amountWithoutVatFormatted": "5,50 €",
        "currency": "EUR",
        "vatPercentage": 10
      },
      "maxQuantity": 2
    }
  ]
}

Devuelve todos los extras compatibles con ese tipo de servicio. El precio es el específico del cliente si lo tiene configurado (un precio de cliente de 0 significa gratis para ese cliente), o el precio base del extra si no. Es la misma regla que aplica la plataforma al guardar el extra en la reserva. Al crear la reserva solo se envían id y quantity: el precio lo pone la plataforma (no se acepta del payload).


8. Crear una reserva

POST /customers/bookings

Traslado por zona (bookingType: "transfer" — es el valor por defecto)

{
  "clientCode": "AGEN01",
  "journey": {
    "bookingType": "transfer",
    "reference": "MI-REF-001",
    "serviceType": "001",
    "dateTime": "2026-09-01T10:00:00Z",
    "timezone": "Europe/Madrid",
    "price": { "amount": 45.0, "currency": "EUR" },
    "passengers": { "totalNumber": 4, "numberOfAdults": 2, "numberOfChildren": 2, "childrenAges": [4, 7] },
    "origin":      { "address": "Aeropuerto de Alicante (ALC)", "latitude": 38.2822, "longitude": -0.5581, "zoneCode": "ALC" },
    "destination": { "address": "Hotel Ejemplo, Benidorm", "latitude": 38.5382, "longitude": -0.1300 },
    "flight": { "flightNumber": "FR2835", "flightDateTime": "2026-09-01T09:30:00Z" },
    "traveler": { "name": "Sr. Ejemplo", "phone": "+34600000000", "email": "cliente@ejemplo.com", "documentNumber": "00000000A" },
    "extras": [ { "id": 3, "quantity": 2 } ],
    "numberOfSuitcases": 4,
    "observations": "Cartel con el nombre",
    "travelerPays": false,
    "amountToChargeDriver": 0,
    "usesHighway": true
  },
  "journey_return": { "…": "mismo formato que journey, opcional (crea la vuelta asociada)" }
}

Disposición (bookingType: "disposal")

{
  "clientCode": "AGEN01",
  "journey": {
    "bookingType": "disposal",
    "reference": "MI-REF-002",
    "serviceType": "001",
    "dateTime": "2026-09-01T10:00:00Z",
    "timezone": "Europe/Madrid",
    "disposal": { "contractedHours": 6 },
    "price": { "amount": 100.0, "currency": "EUR" },
    "passengers": { "totalNumber": 4 },
    "origin":      { "address": "Hotel Ejemplo, Benidorm", "latitude": 38.5382, "longitude": -0.1300 },
    "destination": { "address": "Hotel Ejemplo, Benidorm", "latitude": 38.5382, "longitude": -0.1300 },
    "traveler": { "name": "Sr. Ejemplo", "phone": "+34600000000" }
  }
}

Traslado por distancia (bookingType: "distanceTransfer") — con ida y vuelta

{
  "clientCode": "AGEN01",
  "journey": {
    "bookingType": "distanceTransfer",
    "reference": "MI-REF-003",
    "serviceType": "002",
    "dateTime": "2026-09-20T07:00:00Z",
    "timezone": "Europe/Madrid",
    "distance": { "meters": 48200, "durationSeconds": 2700 },
    "usesHighway": true,
    "price": { "amount": 96.4, "currency": "EUR" },
    "passengers": { "totalNumber": 2, "numberOfAdults": 2 },
    "origin":      { "address": "Aeropuerto de Málaga T3", "latitude": 36.6749, "longitude": -4.4991 },
    "destination": { "address": "Puerto Banús, Marbella", "latitude": 36.4848, "longitude": -4.9530 }
  },
  "journey_return": {
    "bookingType": "distanceTransfer",
    "reference": "MI-REF-003-R",
    "serviceType": "002",
    "dateTime": "2026-09-27T10:00:00Z",
    "timezone": "Europe/Madrid",
    "distance": { "meters": 48200, "durationSeconds": 2700 },
    "price": { "amount": 96.4, "currency": "EUR" },
    "passengers": { "totalNumber": 2, "numberOfAdults": 2 },
    "origin":      { "address": "Puerto Banús, Marbella", "latitude": 36.4848, "longitude": -4.9530 },
    "destination": { "address": "Aeropuerto de Málaga T3", "latitude": 36.6749, "longitude": -4.4991 }
  }
}

Experiencia (bookingType: "experience")

{
  "clientCode": "AGEN01",
  "journey": {
    "bookingType": "experience",
    "reference": "MI-REF-004",
    "serviceType": "001",
    "dateTime": "2026-10-02T08:00:00Z",
    "timezone": "Europe/Madrid",
    "experience": { "id": 17 },
    "price": { "amount": 118.0, "currency": "EUR" },
    "passengers": { "totalNumber": 4, "numberOfAdults": 2, "numberOfChildren": 2, "childrenAges": [6, 9] },
    "origin":      { "address": "Hotel Ejemplo, Sevilla", "latitude": 37.3826, "longitude": -5.9915 },
    "destination": { "address": "Bodegas Ejemplo, Jerez", "latitude": 36.6866, "longitude": -6.1367 },
    "extras": [ { "id": 3, "quantity": 2 } ]
  }
}

Reglas por tipo

transfer disposal distanceTransfer experience
Bloque propio — disposal.contractedHours obligatorio, entero ≥ 1 distance.meters y distance.durationSeconds obligatorios, enteros ≥ 1 (metros y segundos, nunca km) experience.id obligatorio (el id de /customers/experiences; 400 si no existe)
Zonas Se resuelven en los cuatro tipos por zoneCode o, en su defecto, por coordenadas. 404 "Locations not found." si el punto no cae en ninguna zona o el zoneCode no existe ídem ídem ídem
Precio de referencia search-results search-results/disposal search-results/distance experiences
journey_return Permitido 400 — una disposición no tiene vuelta Permitido Permitido
usesHighway Opcional Sin efecto (una disposición no tiene trayecto) Opcional Opcional

Sobre journey_return (ida y vuelta): crea dos reservas asociadas, cada una con su id. La vuelta debe declarar el mismo bookingType que la ida (400 "The field 'journey_return.bookingType' must be …") y cumplir los mismos campos propios del tipo (su distance, su experience.id). Cada tramo lleva su propio precio; los extras de la vuelta se registran a precio 0 (el importe va en la ida). En una experiencia, cada tramo lleva su experiencia y sus actividades.

Comunes a los cuatro tipos:

8.1. Fechas y zona horaria — leed esto antes de integrar

El formato exigido es ISO-8601 con sufijo Z (2026-09-01T10:00:00Z), pero la plataforma no hace conversión de zona. Conviene saberlo con exactitud, porque de lo contrario los servicios quedan programados a una hora distinta de la pedida:

Este comportamiento está identificado y se corregirá para cumplir el contrato UTC descrito arriba. El cambio movería la hora de las reservas existentes, así que no se desplegará sin avisar con antelación y coordinando la fecha con cada integrador. Mientras tanto, la regla válida es la de esta sección.

Respuesta

{ "id": 12345, "message": "Booking created successfully with id 12345." }

Con ida y vuelta: { "id": 12345, "return_id": 12346, "message": "…and return booking with id 12346." }

Guardad el id: es la referencia (bookingReference) para leer, editar y cancelar. Si perdéis el return_id, la lectura de cualquiera de los dos tramos lo devuelve en associatedBookingId (sección 9).


9. Consultar reservas

Listado

GET /customers/bookings?bookingType=transfer,experience&status=PRER,RESE,ASIG&size=100
Parámetro Tipo Defecto Notas
clientCode texto todos Acota a un cliente de la credencial (403 si no os pertenece)
bookingType CSV los cuatro transfer, disposal, distanceTransfer, experience. Tal cual, respetando mayúsculas (400 si no)
status CSV todos los filtrables, incluido FACT Ver tabla de estados más abajo
pickUpDateFrom ISO-8601 con Z ayer 00:00 Límite inferior. Se compara contra la hora local almacenada — ver 8.1
pickUpDateTo ISO-8601 con Z sin límite Para días completos, 23:59:59Z sin desplazar
size entero 400 Máximo 500
after cursor opaco — Lo devuelve la API en links — no construirlo a mano
{
  "links": [ { "rel": "next", "href": "/api-ext/v2/customers/bookings?…", "type": "GET" } ],
  "bookings": [ { "journey": { …reserva… } } ]
}

Paginación: seguir siempre el link con rel == "next". Sin link next → última página.

Detalle

GET /customers/bookings/{bookingReference}

Responde { "journey": { … } }. 404 si la referencia no existe o no os pertenece.

El objeto reserva

{
  "journey": {
    "links": [ { "rel": "self|cancel", "href": "…", "type": "GET|DELETE" } ],
    "id": 12345,
    "reference": "MI-REF-002",
    "associatedBookingId": null,
    "client": { "code": "AGEN01", "name": "Agencia Ejemplo" },
    "status": "RESE",
    "bookingType": "disposal",
    "disposal": {
      "contractedHours": 6,
      "hoursCompleted": null,
      "minutesCompleted": null,
      "kmCompleted": null
    },
    "serviceType": "001",
    "dateTime": "2026-09-01T10:00:00Z",
    "timeZone": "Europe/Madrid",
    "origin":      { "latitude": "38.5382", "longitude": "-0.1300", "address": "…", "type": "STREET_ADDRESS", "zone": { "code": "BEN", "name": "Benidorm" } },
    "destination": { "latitude": "38.5382", "longitude": "-0.1300", "address": "…", "type": "STREET_ADDRESS", "zone": { "code": "BEN", "name": "Benidorm" } },
    "price": {
      "amount": 100.0,
      "amountFormatted": "100,00 €",
      "currency": "EUR",
      "discountPercentage": 10,
      "discountAmount": 10.0,
      "discountAmountFormatted": "10,00 €",
      "netAmount": 90.0,
      "netAmountFormatted": "90,00 €",
      "vatPercentage": 10,
      "vatAmount": 9.0,
      "vatAmountFormatted": "9,00 €",
      "netAmountWithVat": 99.0,
      "netAmountWithVatFormatted": "99,00 €"
    },
    "flight": { "flightNumber": null, "flightDateTime": null },
    "traveler": { "name": "Sr. Ejemplo", "phone": "+34600000000", "email": null },
    "passengers": { "totalNumber": 4, "numberOfAdults": 2, "numberOfChildren": 2, "numberOfInfants": 0 },
    "numberOfSuitcases": 0,
    "travelerPays": false,
    "amountToChargeDriver": null,
    "usesHighway": null,
    "distanceMeters": 12500,
    "durationSeconds": 2700,
    "trackingUrl": "https://…",
    "operator": { "name": "Transportes Ejemplo SL", "phone": "+34600111222", "email": "operaciones@ejemplo.com" },
    "observations": "",
    "extras": [ { "id": 3, "name": "Silla de bebé", "quantity": 2, "unitPrice": 5.5, "totalPrice": 11.0, "currency": "EUR" } ],
    "vehicle": { "code": null, "registrationNumber": null },
    "driver": { "name": null, "phone": null }
  }
}

Estados (status)

Código Significado
PRER Pre-reservada (estado inicial al crear por API)
RESE Reservada (confirmada por la plataforma)
ASIG Con conductor/vehículo asignado
ENTR Conductor en camino a la recogida
ENPO Conductor en el punto de recogida
ABOR Pasajero a bordo
REAL Realizada — estado final
FACT Facturada — la reserva realizada ya ha sido facturada por la plataforma
ANUL Anulada — estado final

FACT entra en el filtro por defecto: una reserva no desaparece del listado al facturarse.


10. Editar una reserva

PUT /customers/bookings/{bookingReference}

Mismo payload que la creación (sin clientCode — se usa el de la reserva). Se editan los cuatro tipos. Reglas:

Respuesta: { "id": 12345, "message": "Booking 12345 edited successfully." }


11. Cancelar una reserva

DELETE /customers/bookings/{bookingReference}
Respuesta Cuándo
200 { "status": "ok", "message": "The booking has been cancelled." } Cancelada
409 "Booking cannot be cancelled because it is already completed." Ya realizada
409 "Booking is already cancelled." Ya anulada (podéis tratarlo como éxito idempotente)
409 "Booking could not be cancelled." No se pudo aplicar — reintentad o contactad
404 Referencia inexistente o ajena

Las condiciones de cancelación (plazos, cargos) son las pactadas comercialmente con la plataforma; la API no aplica penalizaciones por sí misma.


12. Pagos (opcional)

Si vuestra credencial tiene pasarela configurada, podéis iniciar el cobro por Stripe:

POST /customers/payments/initiate-data
{ "amount": 110.0, "journeyId": 12345, "description": "Reserva MI-REF-002" }

Devuelve los datos del PaymentIntent (incluida public_key) para completar el pago con el SDK de Stripe en vuestro front. 400 si la credencial no tiene configuración de Stripe activa; 403 si el servicio no es de vuestros clientes; 404 si no existe.


13. Errores

Formato de error:

{ "message": "Invalid Input parameter.", "description": "The input parameter 'serviceType' not found in request data." }

Discriminad por el código HTTP y, si hace falta, por el message (estable). La description es texto para humanos con el detalle de cada campo y puede cambiar.

Algunas respuestas incluyen además un campo error con un código estable en mayúsculas (p. ej. CONCURRENT_MODIFICATION). No está en todas, así que no lo convirtáis en la única vía de discriminación: usadlo si viene, y el par HTTP + message como criterio de base.

HTTP message Cuándo Acción
401 — Token inválido o caducado Renovar token y reintentar (1 vez)
400 Invalid Input parameter. Payload o parámetro mal formado (la description enumera cada campo) Corregir la petición; no reintentar tal cual
400 Invalid page cursor. Cursor after corrupto Reiniciar paginación desde la primera página
403 Forbidden clientCode que no os pertenece Revisar el código de cliente
404 Not Found / Not found. Reserva, zona o tipo de servicio inexistente o ajeno Comprobar la referencia o el código; no reintentar
404 Price not found. Sin tarifa para la búsqueda (zona, disposición o distancia) Sin cobertura: no reintentar (traslados por zona: probar otras coordenadas)
404 Locations not found. Coordenadas fuera de todas las zonas o zoneCode inexistente, en cualquier tipo de reserva Sin cobertura en ese punto
409 Conflict Fuera de la ventana de edición; cancelación imposible Leer la description y decidir
409 Booking modified concurrently. Dos peticiones simultáneas sobre la misma reserva Re-GET y reintentar
500 The user is not configured correctly. Problema de alta de la credencial Contactar con la plataforma
500 Server error. Error interno Reintentar con backoff exponencial

14. Buenas prácticas (resumen)

  1. Tarificad siempre antes de crear: el price.amount de la reserva debería salir de search-results, search-results/disposal, search-results/distance o experiences, no de una tarifa cacheada.
  2. 401 → renovar y reintentar una vez, en cualquier operación.
  3. Fechas siempre en formato YYYY-MM-DDTHH:MM:SSZ, y timezone con la zona IANA del punto de recogida. Enviad la hora local de recogida con el sufijo Z y no esperéis que el dateTime leído coincida con el enviado: leed la sección 8.1, es el punto donde más se equivocan las integraciones nuevas.
  4. Paginación con rel: next; nunca construir el cursor after a mano.
  5. Backoff en 5xx/timeout; jamás reintentar un 4xx sin corregir la petición.
  6. Guardad el id devuelto al crear: es la única referencia para leer/editar/cancelar. Vuestra reference propia viaja con la reserva pero no sirve para buscarla.
  7. Disposiciones, traslados por distancia y experiencias se editan enviando su bookingType explícito — sin él, la edición se valida como traslado y fallará por el tipo.
  8. Para el seguimiento del servicio, consultad el detalle: status, driver y vehicle se actualizan a medida que la plataforma gestiona la reserva.
  9. Deduplicad por id al paginar el listado: es lo que os protege de que una reserva llegue dos veces al reanudar un cursor.

15. Cambios respecto a la versión 1.0 (agosto → septiembre 2026)

Lo que cambia para quien ya integró con la versión 1.0 de esta guía. Los cambios marcados con ⚠️ pueden requerir ajustes en vuestro lado.

Nuevo

⚠️ Cambios de contrato en respuestas ya publicadas

Cambios compatibles


Especificación técnica completa: openapi-customers-v2.yaml (importable en Postman).

Nota sobre los identificadores booking/journey. Las rutas (/customers/bookings), la clave journey y el campo bookingReference son nombres del contrato HTTP y nada más. La clave journey envuelve los cuatro tipos de reserva, aunque una disposición no sea un "trayecto": renombrarla rompería a los integradores vivos — sería una v3.