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


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 } ]
    }
  ]
}

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:


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:

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 }
  }
}

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:

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)

  1. Tarificad siempre antes de crear: el price.amount de la reserva debería salir de search-results / search-results/disposal, no de una tarifa cacheada.
  2. 401 → renovar y reintentar una vez, en cualquier operación.
  3. Fechas siempre en UTC (YYYY-MM-DDTHH:MM:SSZ) y el timezone IANA del punto de recogida.
  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. Las disposiciones se editan enviando bookingType: "disposal" 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.

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 tanto traslados como disposiciones, aunque una disposición no sea un "trayecto": renombrarla rompería a los integradores vivos — sería una v3.