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
- El token caduca a los 900 segundos. Renovadlo de forma proactiva ~60 s antes.
- Cualquier respuesta
401→ renovar token y reintentar la petición una vez. No hay que tratar el 401 como error de negocio. - El token se envía en todas las llamadas:
Authorization: Bearer <access_token>.
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})clientCodeno se usa: si se envía, se ignora en silencio. La reserva ya viene con suclient, y el control de acceso es el mismo (404si 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 } ]
}
]
}
404 "Price not found."→ no hay tarifa para esas zonas: no hay cobertura.- Si el cliente está exento de IVA,
vatPercentagees0y ambos importes coinciden. - El
codedel resultado es elserviceTypeque después se envía al crear la reserva.
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:
- El precio se compone de paquete + horas extra:
packageHourshoras apackagePrice, y cada hora por encima aextraHourPrice.extraHoursya viene calculado. - Con una tarifa plana sin paquete,
packageHoursyextraHourssonnull. includedKmson los kilómetros incluidos en el precio anunciado. Al finalizar el servicio, los kilómetros realizados por encima se facturan aextraKmPrice(el precio final puede recalcularse con las horas y kilómetros reales — sección 10).- No multipliquéis
includedKmpor las horas: ya es el total incluido. salePriceconserva hasta 4 decimales (la precisión de las tarifas);salePriceWithVatse redondea a 2 (lo que se cobra).404 "Price not found."→ el cliente no tiene tarifa de disposición para ese tipo de servicio y ese número de horas.
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 €"
}
}
- La respuesta devuelve la distancia y la duración empleadas en el cálculo: son las que
hay que enviar en
journey.distanceal crear la reserva, donde son obligatorias. - El precio depende del tipo de servicio y de los metros; la duración no interviene en el precio, pero la plataforma la necesita para planificar el servicio.
404 "Price not found."→ el cliente no tiene tarifa por distancia para ese tipo de servicio.
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 €"
}
}
]
}
price.salePricees exactamente lo que hay que enviar enjourney.price.amountal crear la reserva. No recalculéis el total por vuestra cuenta: sipricePerPassengerestrueel total es adultos × precio adulto + niños × precio niño; si esfalse, el precio es por servicio. La plataforma ya aplica la regla que toque.adultPriceWithoutVatychildPriceWithoutVatson el desglose por pasajero, para mostrarlo; van sin IVA.activitiesson los nombres de las actividades incluidas. Nunca se envían al crear: la reserva las toma de la experiencia.- Sin periodo de tarifas que cubra la fecha, o sin experiencias vendibles para ese cliente:
{ "experiences": [] }(no es error). - El
idde la experiencia es el que va enjourney.experience.idal crear.
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:
- Obligatorios:
reference,serviceType,dateTime(UTC),timezone(IANA),price.amount,passengers.totalNumber,origin.address/latitude/longitude,destination.address/latitude/longitude(el destino es obligatorio también en disposiciones — usad el punto de recogida si no hay otro). journey.bookingTypeausente o vacío ("") equivale atransfer(compatibilidad con integraciones anteriores a los demás tipos). Valor desconocido →400 "Invalid journey.bookingType, expected: transfer, disposal, distanceTransfer, experience."childrenAgesse anexa a las observaciones del servicio.- La reserva se crea en estado
PRER(pre-reservada). - El alta es atómica: si algo falla a mitad, no queda nada creado (ni la ida sin su vuelta).
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:
- Al crear, se toma el reloj que enviáis y se guarda tal cual como hora local del
punto de recogida.
2026-09-01T10:00:00Zqueda como las 10:00 locales. journey.timezonees obligatorio, se valida como zona IANA y no se aplica. EnviarEurope/MadridoAsia/Tokyoproduce exactamente el mismo resultado.- Regla práctica: enviad la hora local de recogida con el sufijo
Z. Es lo que hacen hoy las integraciones en producción y lo que hace que el servicio se ejecute a la hora correcta. - Al leer, el
dateTimedevuelto no coincide con el enviado: la hora almacenada se convierte a UTC usando la zona del servidor. Con servidor en España se recibe 2 h menos en horario de verano y 1 h menos en invierno. El campotimeZonerefleja esa zona del servidor, no la del servicio. - En los filtros del listado,
pickUpDateFrom/pickUpDateTose comparan contra esa hora local. Para pedir días completos usad00:00:00Zy23:59:59Zsin aplicar ningún desplazamiento: si enviáis la ventana desplazada (p. ej.22:00:00Zdel día anterior), perderéis en silencio los servicios de las últimas horas de cada día.
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 }
}
}
bookingTypedevuelve el valor público (transfer,disposal,distanceTransfer,experience): lo que leéis sirve tal cual para filtrar y para crear.serviceTypedevuelve el código del tipo de servicio ("001"), el mismo que se envía en el alta y que devuelve la tarificación encode. No es elidinterno.associatedBookingIdes eliddel otro tramo de un ida y vuelta, onullsi la reserva no tiene pareja. Es el mismo valor que elreturn_iddel alta. Cuando hay pareja,linksincluye además{ "rel": "associated", "href": "…/bookings/{id}", "type": "GET" }. No se indica cuál de los dos tramos es la ida: comparad losdateTime.- El bloque
clientdice a qué cliente vuestro pertenece la reserva. Con una credencial de un solo cliente es siempre el mismo; con varias es lo que permite atribuir cada reserva.codecruza con/customers/clients(sección 3) y conclientCodedel alta. - Bloques propios de cada tipo, presentes solo en su tipo:
disposal(disposiciones):hoursCompleted,minutesCompletedykmCompletedse rellenan cuando la plataforma cierra el servicio con los datos reales.distance(traslados por distancia):{ "meters": 48200, "durationSeconds": 2700 }, los valores con los que se tarificó.experience(experiencias):{ "id": 17, "name": "Ruta de bodegas", "pricePerPassenger": true, "activities": [ { "id": 5, "name": "Visita a bodega", "sortOrder": 1 } ] }. Del proveedor de cada actividad no se expone nada.- El bloque
pricees el precio del servicio solo: no incluye extras (van enextras), autopista ni recargos. amount: tarifa sin IVA y sin descuento.discountPercentage/discountAmount: descuento aplicado (0si no hay).netAmount: tarifa sin IVA con el descuento aplicado (amount − discountAmount).vatPercentage/vatAmount: IVA del servicio, calculado sobrenetAmount.netAmountWithVat: lo que se cobra por el servicio (netAmount + vatAmount). Suma los importes ya redondeados a 2 decimales, así que cuadra con los valores mostrados.- Cada importe trae su versión
…Formattedcon el símbolo de la moneda. Si el servicio no tiene tarifa, todos los importes vienen anull; los porcentajes, a0. usesHighwayesnullen disposiciones (no hay trayecto).typeyzoneen origen/destino aparecen en los cuatro tipos si el servicio tiene zona en ese extremo. Las reservas creadas antes de septiembre de 2026 en disposiciones y traslados por distancia pueden no tenerla: en ese caso las dos claves faltan (no vienen anull).vehicleydriverse rellenan cuando la plataforma asigna el servicio — también cuando lo ejecuta un proveedor colaborador: veréis el conductor y la matrícula asignados, sin distinción de quién opera.trackingUrles el enlace de seguimiento en vivo para vuestro viajero, si el servicio dispone de él. En el detalle se genera/incluye siempre que el seguimiento esté disponible; en el listado solo aparece si ya se había generado. Puede caducar al terminar el servicio.null= sin seguimiento disponible.- El bloque
operatoridentifica al proveedor colaborador que ejecuta el servicio. Solo aparece si vuestra credencial lo tiene habilitado (se acuerda con la plataforma) y solo en los servicios ejecutados por un colaborador: en los que opera directamente vuestro proveedor, la clave no aparece. No incluye datos fiscales ni económicos. distanceMetersydurationSecondsson estimaciones del trayecto. En el listado pueden venir anullsi aún no están calculadas — el detalle las calcula siempre que sea posible. En una disposición,durationSecondsson las horas contratadas.
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:
- Ventana de edición: cada cliente tiene un mínimo de minutos de antelación. Fuera de
plazo →
409con la fecha límite en ISO-8601:"Booking cannot be edited. The service can only be edited until 2026-09-01T08:00:00Z." - El tipo no cambia: enviar un
bookingTypedistinto del guardado →400 "The field 'journey.bookingType' cannot be changed on an existing booking. Booking 12345 is a disposal."Si necesitáis cambiar de tipo: cancelad y cread de nuevo. - Sin
bookingTypeen el payload se asumetransfer: para editar una disposición, un traslado por distancia o una experiencia hay que enviar subookingTypeexplícito (y su bloque propio:disposal.contractedHours,distance,experience.id). - La edición no cambia el estado de la reserva: una reserva
RESEoASIGsigue en su estado tras editarla. La plataforma la marca internamente como "con cambios del cliente pendientes de validar". journey_returnno se procesa en la edición y la vuelta no se toca: la pareja es una reserva independiente con su propioid(lo tenéis enassociatedBookingId) — editadla por separado, o canceladla con su propioDELETE.- La edición es atómica: si responde
400, la reserva queda exactamente como estaba. - Zonas: se vuelven a resolver con el payload (mismo
404que en el alta). - En las disposiciones, las horas y kilómetros realizados los fija la plataforma al cerrar el servicio; si difieren de lo contratado, el precio final se recalcula con la tarifa vigente (horas extra y km extra — sección 6).
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)
- Tarificad siempre antes de crear: el
price.amountde la reserva debería salir desearch-results,search-results/disposal,search-results/distanceoexperiences, no de una tarifa cacheada. - 401 → renovar y reintentar una vez, en cualquier operación.
- Fechas siempre en formato
YYYY-MM-DDTHH:MM:SSZ, ytimezonecon la zona IANA del punto de recogida. Enviad la hora local de recogida con el sufijoZy no esperéis que eldateTimeleído coincida con el enviado: leed la sección 8.1, es el punto donde más se equivocan las integraciones nuevas. - Paginación con
rel: next; nunca construir el cursoraftera mano. - Backoff en 5xx/timeout; jamás reintentar un 4xx sin corregir la petición.
- Guardad el
iddevuelto al crear: es la única referencia para leer/editar/cancelar. Vuestrareferencepropia viaja con la reserva pero no sirve para buscarla. - Disposiciones, traslados por distancia y experiencias se editan enviando su
bookingTypeexplícito — sin él, la edición se valida como traslado y fallará por el tipo. - Para el seguimiento del servicio, consultad el detalle:
status,driveryvehiclese actualizan a medida que la plataforma gestiona la reserva. - Deduplicad por
idal 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
- Dos tipos de reserva más:
distanceTransferyexperience, en los mismos endpoints (secciones 1, 8, 9 y 10). POST /customers/search-results/distance(6.1) yPOST /customers/experiences(6.2).associatedBookingIdy el enlacerel: associateden la lectura de cualquier reserva.- Estado
FACT(facturada), filtrable y presente en el filtro por defecto. - Bloques
distanceyexperienceen la lectura, solo en su tipo. - (Octubre 2026) El bloque
pricede la lectura añade descuento, neto, IVA y total con IVA del servicio (sección 9).amount,amountFormattedycurrencyno cambian.
⚠️ Cambios de contrato en respuestas ya publicadas
bookingTypeen la lectura devuelve el valor público (transfer,disposal, …) en vez del nombre en castellano (Traslado,Disposicion).serviceTypeen la lectura devuelve el código ("001") en vez delidinterno (9). Si guardabais elid, cruzadlo concodeensearch-results.- Los listados sin
statusexplícito incluyen ahora las reservas facturadas (FACT). - La zona se resuelve y se exige también en disposiciones y traslados por distancia: altas
con coordenadas fuera de zona que antes entraban responden ahora
404 "Locations not found.". EnviarzoneCodeevita depender de la coordenada. - La paginación del listado tenía un fallo por el que algunas reservas con la misma fecha y
hora quedaban fuera de todas las páginas. Corregido: pueden aparecer reservas antiguas
que nunca habíais recibido. Deduplicad por
id.
Cambios compatibles
POST /customers/extras/by-service-typepideserviceTypecon el código;serviceTypeIdse sigue aceptando por compatibilidad pero ya no forma parte del contrato. Devuelve todos los extras compatibles con el tipo de servicio, al precio base los que el cliente no tenga configurados (antes solo salían los configurados).journey_returnse admite también endistanceTransferyexperience.- Editar la ida de un ida y vuelta ya no anula la vuelta.
- La edición conserva el estado de la reserva (antes volvía a
PRER). - Alta y edición son atómicas: un
400no deja cambios a medias. - La tarificación anuncia IVA
0a los clientes exentos también en traslados y extras.
Especificación técnica completa: openapi-customers-v2.yaml (importable en Postman).
Nota sobre los identificadores
booking/journey. Las rutas (/customers/bookings), la clavejourneyy el campobookingReferenceson nombres del contrato HTTP y nada más. La clavejourneyenvuelve los cuatro tipos de reserva, aunque una disposición no sea un "trayecto": renombrarla rompería a los integradores vivos — sería una v3.
