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.0 (agosto 2026)
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 dos tipos de reserva (bookingType):
bookingType |
Qué es | Precio |
|---|---|---|
transfer |
Traslado: 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 — no de las zonas |
Flujo típico:
GET /customers/clients ← vuestros clientes y sus códigos
GET /customers/zones ← zonas de cobertura (solo traslados)
│
▼
POST /customers/search-results ← tarificar un traslado
POST /customers/search-results/disposal ← tarificar una disposición
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.
4. Zonas (solo traslados)
Las zonas determinan la cobertura y el precio de los traslados. Las disposiciones no las usan.
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
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 en
UTC estricto; pickupTimezone una zona IANA válida.
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.
7. Extras
POST /customers/extras/by-service-type
{ "serviceTypeId": 9, "clientCode": "AGEN01" }
{
"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
}
]
}
El precio es el específico del cliente si lo tiene, o el precio base del extra. 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 (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, solo transfer (opcional)" }
}
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" }
}
}
Reglas por tipo
transfer |
disposal |
|
|---|---|---|
disposal.contractedHours |
— | Obligatorio, entero ≥ 1 |
| Zonas | Se resuelven por zoneCode o, en su defecto, por coordenadas. 404 si el punto no cae en ninguna zona |
No se resuelven: no hay error de cobertura |
journey_return |
Permitido (crea dos reservas asociadas) | 400 — una disposición no tiene vuelta |
usesHighway |
Opcional | Sin efecto (es propio del trayecto) |
Comunes a ambos 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 equivale atransfer(compatibilidad con integraciones anteriores a las disposiciones).journey_return.bookingType, si se envía, debe sertransfer.childrenAgesse anexa a las observaciones del servicio.- La reserva se crea en estado
PRER(pre-reservada).
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.
9. Consultar reservas
Listado
GET /customers/bookings?bookingType=transfer,disposal&status=PRER,RESE,ASIG&size=100
| Parámetro | Tipo | Defecto | Notas |
|---|---|---|---|
bookingType |
CSV | ambos | transfer, disposal. En minúsculas (400 si no) |
status |
CSV | todos | Ver tabla de estados más abajo |
pickUpDateFrom |
ISO-8601 UTC | ayer 00:00 | Límite inferior de la fecha de recogida |
pickUpDateTo |
ISO-8601 UTC | sin límite | |
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",
"status": "RESE",
"bookingType": "Disposicion",
"disposal": {
"contractedHours": 6,
"hoursCompleted": null,
"minutesCompleted": null,
"kmCompleted": null
},
"serviceType": 9,
"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": "…" },
"price": { "amount": 100.0, "amountFormatted": "100,00 €", "currency": "EUR" },
"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 }
}
}
- El bloque
disposalsolo aparece en disposiciones.hoursCompleted,minutesCompletedykmCompletedse rellenan cuando la plataforma cierra el servicio con los datos reales. usesHighwayesnullen disposiciones (no hay trayecto).zoneen origen/destino solo aparece en traslados.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 |
ANUL |
Anulada — estado final |
10. Editar una reserva
PUT /customers/bookings/{bookingReference}
Mismo payload que la creación (sin clientCode — se usa el de la reserva). 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
bookingType: disposalsobre un traslado (o al revés) →400 "The field 'journey.bookingType' cannot be changed on an existing booking."Si necesitáis cambiar de tipo: cancelad y cread de nuevo. - Sin
bookingTypeen el payload se asumetransfer: para editar una disposición hay que enviarbookingType: "disposal"(y susdisposal.contractedHours). journey_returnno se procesa en la edición: la vuelta es una reserva independiente con su propioid— editadla por separado.- 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.
| 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 |
Reserva/zona inexistente o ajena | Comprobar la referencia; no reintentar |
| 404 | Price not found. |
Sin tarifa para la búsqueda | Sin cobertura: no reintentar (traslados: probar otras coordenadas) |
| 404 | Locations not found. |
Coordenadas fuera de todas las zonas (traslados) | 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, no de una tarifa cacheada. - 401 → renovar y reintentar una vez, en cualquier operación.
- Fechas siempre en UTC (
YYYY-MM-DDTHH:MM:SSZ) y eltimezoneIANA del punto de recogida. - 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. - Las disposiciones se editan enviando
bookingType: "disposal"explí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.
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 tanto traslados como disposiciones, aunque una disposición no sea un "trayecto": renombrarla rompería a los integradores vivos — sería una v3.
