{"openapi":"3.1.0","info":{"title":"RentaCarFurgo API — alquiler de furgonetas (Murcia, España)","version":"1.0.0","summary":"Endpoints que consume la app móvil de RentaCarFurgo.","description":"API **privada de la app móvil** de RentaCarFurgo (alquiler de furgonetas en Murcia y toda España).\n\nTodas las rutas `/api/app/*` exigen el **token de sesión del cliente** (`Authorization: Bearer <access_token>` de Supabase Auth); la propiedad de cada recurso la decide la seguridad a nivel de fila, así que un recurso ajeno responde 404, nunca 403.\n\n**No hay API pública de escritura**: reservar, modificar o cancelar lo hace una persona desde la web o por teléfono. Para consultar flota, precios e información de empresa sin credenciales está el **servidor MCP** en `/api/mcp` (ver `/.well-known/mcp`) y `/llms.txt`.\n\nLos errores siempre son JSON con la misma forma (`ok`, `error`, `message`, `hint`), incluida una ruta inexistente bajo `/api/` y un método no permitido.\n\n## Versión y deprecación\n\nVersión actual: **v1**. Las rutas responden en las dos formas —`/api/app/...` y `/api/app/v1/...`— y son **la misma implementación**: el alias existe para poder versionar sin romper a los clientes que ya llaman a la ruta sin versión.\n\nSi alguna vez se retira una operación, se anunciará **antes** con las cabeceras estándar `Deprecation` (fecha en que pasa a estar desaconsejada) y `Sunset` (fecha a partir de la cual deja de responder), más un `Link` con `rel=\"deprecation\"` a la explicación. Ninguna ruta desaparece sin ese aviso previo.\n\n## Límite de tasa\n\nLos endpoints públicos (sin credenciales) responden con `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` y `RateLimit-Policy`. El límite real es de **60 peticiones por minuto y dirección IP**; al pasarlo se devuelve **429** con `Retry-After`.","contact":{"name":"RentaCarFurgo","email":"contacto@rentacarfurgo.com","url":"https://rentacarfurgo.com/developers"},"license":{"name":"Uso privado","identifier":"LicenseRef-Proprietary"}},"servers":[{"url":"https://rentacarfurgo.com","description":"Producción · rutas sin versión (las que usa la app hoy)"},{"url":"https://rentacarfurgo.com/api/app/v1","description":"Producción · alias versionado v1 (misma implementación)"}],"tags":[{"name":"app","description":"Endpoints de la app móvil (token de cliente)."},{"name":"agentes","description":"Superficie legible por máquinas, sin credenciales."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Token de sesión del cliente (Supabase Auth)."}},"schemas":{"Error":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","enum":[false],"description":"Siempre false en un error."},"error":{"type":"string","description":"Código estable del error (not_authenticated, not_found, no_payment…)."},"message":{"type":"string","description":"Frase legible que explica qué ha pasado."},"hint":{"type":"string","description":"Qué hacer al respecto, cuando se puede decir algo útil."}}}}},"paths":{"/api/app/quote":{"post":{"operationId":"appQuote","summary":"Precio de un alquiler","description":"Precio real para una furgoneta y unas fechas, con el mismo motor que la web. El canal se resuelve en servidor: no se puede falsificar desde el cliente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"total":{"type":"number","description":"Precio total del alquiler, en euros."},"per_day":{"type":"number","description":"Precio por día, en euros."},"days":{"type":"integer","description":"Días de alquiler."},"disc_pct":{"type":"number","description":"Descuento aplicado, en porcentaje."},"currency":{"type":"string","enum":["EUR"]}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vehicle_id","pickup_at","return_at"],"properties":{"vehicle_id":{"type":"string","format":"uuid","description":"Furgoneta."},"pickup_at":{"type":"string","format":"date-time","description":"Recogida (ISO 8601)."},"return_at":{"type":"string","format":"date-time","description":"Devolución (ISO 8601)."},"age_band":{"type":"string","description":"Tramo de edad del conductor.","examples":["26-60"]}}}}}}}},"/api/app/bookings/express-quote":{"post":{"operationId":"appExpressQuote","summary":"Precio de Alquiler Express","description":"Presupuesto de un Alquiler Express (recogida rápida) para el cliente autenticado.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"total":{"type":"number","description":"Total del Alquiler Express, en euros."},"per_day":{"type":"number"},"km_included":{"type":"integer","description":"Kilómetros incluidos."},"deposit":{"type":"number","description":"Fianza, en euros."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/bookings/{id}/confirm-payment":{"post":{"operationId":"appConfirmPayment","summary":"Confirmar el pago de una reserva","description":"Comprueba contra el proveedor que el pago existe de verdad y asienta la reserva con los mismos efectos que la web (apunte de pago, fianza, cupón, referido y puntos). Idempotente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"status":{"type":"string","description":"Estado en el que queda la reserva.","examples":["pending_confirmation","paid"]},"booking_id":{"type":"string","format":"uuid"}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}}]}},"/api/app/bookings/{id}/cancel":{"post":{"operationId":"appCancelBooking","summary":"Cancelar una reserva","description":"Cancela una reserva del cliente aplicando la política de cancelación vigente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"refund":{"type":"number","description":"Importe a devolver, en euros."},"penalty_pct":{"type":"number","description":"Penalización aplicada, en porcentaje."},"status":{"type":"string","examples":["cancelled"]}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}}]}},"/api/app/bookings/{id}/cancellation-policy":{"get":{"operationId":"appBookingCancellationPolicy","summary":"Política de cancelación de una reserva","description":"Qué se devolvería si ESTA reserva se cancelara ahora, con la política entera y el tramo que aplica.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"refund_pct":{"type":"number","description":"% del alquiler que se devuelve con esta antelación."},"applied_tier":{"type":"object","description":"Tramo aplicable ahora.","properties":{"min_hours":{"type":"number"},"refund_pct":{"type":"number"}}},"penalty_applicable":{"type":"number","description":"Lo que se retendría de verdad, en euros (nunca más de lo cobrado)."},"refund_estimate":{"type":"number","description":"Importe que se devolvería, en euros."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}}]}},"/api/app/bookings/{id}/extend":{"post":{"operationId":"appExtendBooking","summary":"Ampliar una reserva en curso","description":"Alarga una reserva `active` (con la furgoneta ya entregada). Con `preview: true` solo cotiza. El servidor decide la RUTA: `same_vehicle` amplía tu reserva; `vehicle_switch` significa que tu furgoneta no está libre y lo que se puede hacer es contratar otra para el tramo nuevo, lo que crea una reserva SEPARADA. El precio nunca viene del cliente. Al confirmar hay dos finales posibles: `status: \"extended\"` (cobrada y ampliada) o `status: \"pending_approval\"` (firmada, importe RETENIDO sin cobrar y esperando que el equipo la apruebe).","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"preview":{"type":"boolean","description":"true si la respuesta es una cotización."},"route":{"type":"string","enum":["same_vehicle","vehicle_switch"],"description":"Lo decide el servidor, nunca la app."},"status":{"type":"string","enum":["extended","pending_approval"],"description":"Solo al confirmar: cobrada y ampliada, o retenida esperando aprobación."},"extra_amount":{"type":"number","description":"PRECIO de la ampliación, con extras y descuentos ya dentro. No sumarle nada."},"card_amount":{"type":"number","description":"Lo que mueve la tarjeta: `extra_amount` menos el saldo aplicado."},"wallet_balance":{"type":"number","description":"Saldo libre del cliente."},"wallet_applied":{"type":"number","description":"Saldo que entraría con lo pedido."},"coupon_discount":{"type":"number","description":"Descuento del cupón, si se aplicó."},"new_total":{"type":"number","description":"Total de la reserva después de ampliar."},"held":{"type":"number","description":"Importe RETENIDO (no cobrado) cuando `status` es `pending_approval`."},"sla_due_at":{"type":"string","format":"date-time","description":"Cuándo hay respuesta, contado en horario laboral."},"new_return_at":{"type":"string","format":"date-time"},"alternatives":{"type":"array","description":"Solo en `vehicle_switch`: furgonetas libres para el tramo nuevo, con su precio.","items":{"type":"object"}}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["new_return_at"],"properties":{"new_return_at":{"type":"string","format":"date-time","description":"Nueva devolución. Tiene que ser posterior a la actual."},"preview":{"type":"boolean","description":"true = solo cotizar, no toca nada."},"vehicle_id":{"type":"string","format":"uuid","description":"Solo al confirmar la ruta `vehicle_switch`: una de las alternativas que devolvió el preview."},"signer_name":{"type":"string","description":"Obligatorio al confirmar."},"idempotency_key":{"type":"string","description":"Obligatorio al confirmar: impide cobrar dos veces si la petición se repite."},"extras":{"type":"object","description":"Extras a añadir: `{ \"<extra_id>\": unidades }`.","additionalProperties":{"type":"integer"}},"coupon":{"type":"string","description":"Cupón, solo si está marcado como válido para ampliaciones."},"wallet_apply":{"type":"number","description":"Euros de saldo del monedero que se quieren aplicar."},"payment_method_id":{"type":"string","format":"uuid","description":"Tarjeta guardada con la que mover el importe."}}}}}}}},"/api/app/bookings/{id}/extend/days":{"get":{"operationId":"appExtendBookingDays","summary":"Días a los que se puede ampliar","description":"Los días del mes a los que ESTA reserva se puede ampliar y, por cada día, el RANGO DE HORAS válido (`min_time`/`max_time`, Europe/Madrid). Todo resuelto por el servidor con la misma función que valida al confirmar: tramo entero libre, margen de limpieza, mantenimientos, furgoneta en taller, tope de días configurado y que el día sea posterior a la devolución actual. `available` significa «hay alguna hora válida ese día». La app no tiene que aplicar ninguna de esas reglas. No se dice POR QUÉ un día no está: eso diría dónde tiene reserva otro cliente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"year":{"type":"integer"},"month":{"type":"integer","description":"1-12."},"max_days":{"type":"integer","description":"Máximo que se puede ampliar de una vez, decidido por el negocio. Ya viene aplicado en `days`."},"current_return_at":{"type":"string","format":"date-time","description":"Devolución actual: el primer día ampliable es el siguiente."},"days":{"type":"array","description":"Un día por fecha del mes, en orden.","items":{"type":"object","required":["date","available"],"properties":{"date":{"type":"string","format":"date","examples":["2026-09-18"]},"available":{"type":"boolean","description":"Si ese día hay ALGUNA hora válida para la nueva devolución."},"min_time":{"type":"string","nullable":true,"description":"Primera hora válida ('HH:MM', Europe/Madrid), o null si no hay ninguna.","examples":["08:00"]},"max_time":{"type":"string","nullable":true,"description":"Última hora válida ('HH:MM', Europe/Madrid) — con el margen de limpieza de la siguiente reserva ya descontado.","examples":["12:00"]}}}},"time_step_minutes":{"type":"integer","description":"Paso del desplegable de horas, el mismo del horario del buscador.","examples":[30]}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}},{"name":"year","in":"query","required":false,"description":"Año del mes a consultar. Sin él, el mes actual.","schema":{"type":"integer","examples":[2026]}},{"name":"month","in":"query","required":false,"description":"Mes, 1-12. Sin él, el mes actual.","schema":{"type":"integer","minimum":1,"maximum":12}}]}},"/api/app/payment-methods":{"get":{"operationId":"appPaymentMethods","summary":"Tarjetas guardadas del cliente","description":"Las tarjetas con las que se puede pagar off-session, cada una con su `id` estable. Ya filtradas por el proveedor de pago activo: las de un proveedor apagado no salen, porque no se puede cobrar con ellas. Nunca viajan el token ni el identificador de la operación recurrente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"payment_methods":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Lo que se manda como `payment_method_id` al pagar o ampliar."},"brand":{"type":"string","nullable":true,"examples":["visa"]},"last4":{"type":"string","nullable":true,"description":"Puede ser null: Redsys no siempre lo devuelve."},"exp":{"type":"string","nullable":true,"description":"'MM/AA'."},"is_primary":{"type":"boolean"},"provider":{"type":"string","enum":["redsys","stripe"]}}}}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/payment-methods/start":{"get":{"operationId":"appPaymentMethodStartStatus","summary":"En qué quedó el alta de la tarjeta","description":"Estado del alta que devolvió el POST. `pending` = sigue abierta (volver a preguntar). `done` = el proveedor contestó: `tokenized` dice si hay referencia reutilizable, `already_saved` si esa tarjeta ya estaba en la cuenta (entonces la lista no crece, y eso NO es un error) y `payment_method_id` es el id que se manda al pagar o al ampliar. `failed` = rechazada o abandonada. Hace falta porque la tarjeta la guarda la notificación del proveedor, unos segundos después de que el cliente vea «autorizada».","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"status":{"type":"string","enum":["pending","done","failed"]},"tokenized":{"type":"boolean","description":"El proveedor devolvió referencia reutilizable."},"already_saved":{"type":"boolean","description":"Esa tarjeta ya estaba en la cuenta."},"payment_method_id":{"type":"string","format":"uuid","nullable":true,"description":"Tarjeta usable resultante del alta."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"intent","in":"query","required":true,"description":"El `intent_id` que devolvió el POST.","schema":{"type":"string","format":"uuid"}}]},"post":{"operationId":"appStartPaymentMethod","summary":"Añadir una tarjeta nueva","description":"Empieza el alta de una tarjeta NUEVA, para poder pagar (o ampliar) con ella después. Con Redsys devuelve `mode: \"redsys\"` y una `url` que hay que abrir en una vista web: la tarjeta se teclea en el TPV del banco, con su 3-D Secure, y lo que queda guardado es una referencia reutilizable (la preautorización de 1 € se anula sola). Cuando el navegador llegue a `return_url`, volver a pedir `GET /api/app/payment-methods` y usar el `id` nuevo como `payment_method_id`. Si NO se guardó, el navegador llega a la misma ruta con `status=error&motivo=<motivo>` (pasando antes por `/api/payments/vuelta-tarjeta`); `motivo` es uno de `denegada`, `sin_saldo`, `datos`, `tarjeta_caducada`, `no_admitida`, `3ds`, `cancelada`, `sesion_caducada`, `banco_no_disponible`, `pasarela`, `desconocido`. Con Stripe activo devuelve `mode: \"stripe\"` y su `client_secret`, sin URL.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"mode":{"type":"string","enum":["redsys","stripe"],"description":"Qué proveedor va a atender el alta."},"url":{"type":"string","format":"uri","description":"Solo en `redsys`: abrir en una vista web."},"return_url":{"type":"string","format":"uri","description":"Solo en `redsys`: a dónde vuelve el navegador al acabar."},"intent_id":{"type":"string","format":"uuid","description":"Solo en `redsys`: el intento de tokenización creado."},"client_secret":{"type":"string","description":"Solo en `stripe`: SetupIntent `off_session` para su SDK."},"publishable_key":{"type":"string","description":"Solo en `stripe`."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"return_path":{"type":"string","description":"Ruta de ESTE sitio a la que volver al acabar (tiene que empezar por `/`). Sirve para detectar el final del viaje desde la vista web.","examples":["/cuenta?t=pagos&card_added=1"]}}}}}}}},"/api/app/bookings/{id}/inspections":{"get":{"operationId":"appBookingInspections","summary":"Partes de entrega y devolución","description":"Fotos, combustible, kilómetros y daños registrados en el check-in y el check-out de la reserva.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"checkin":{"type":"object","description":"Parte de entrega: fotos, combustible, kilómetros y daños."},"checkout":{"type":"object","description":"Parte de devolución, con la misma forma."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) de la reserva.","schema":{"type":"string","format":"uuid"}}]}},"/api/app/bookings/{id}/checkin":{"post":{"operationId":"appBookingCheckin","summary":"Check-in del cliente","description":"Envía el parte de entrega desde la app (fotos, combustible, kilómetros y firma).","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"checkin_id":{"type":"string","format":"uuid","description":"Parte creado."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Reserva: su identificador (uuid) o su código (RCF-AAAA-NNNN); valen los dos.","schema":{"type":"string"}}]}},"/api/app/contracts/sign":{"post":{"operationId":"appSignContract","summary":"Firmar el contrato","description":"Registra la firma del contrato de alquiler y arma el bloqueo de precio de la reserva.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"contract_id":{"type":"string","format":"uuid"},"pdf_url":{"type":"string","format":"uri","description":"Contrato firmado en PDF."},"price_locked_until":{"type":"string","format":"date-time","description":"Hasta cuándo se mantiene el precio."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/delivery/quote":{"post":{"operationId":"appDeliveryQuote","summary":"Precio de entrega a domicilio","description":"Coste de llevar la furgoneta a una dirección, según la distancia real.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"price":{"type":"number","description":"Coste de la entrega, en euros."},"distance_km":{"type":"number","description":"Distancia real, en kilómetros."},"available":{"type":"boolean","description":"Si se llega a esa dirección."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/cancellation-policy/defaults":{"get":{"operationId":"appCancellationDefaults","summary":"Política de cancelación general","description":"Tramos configurados por el negocio, sin necesidad de una reserva. Aplica el de mayor antelación que se cumpla; el último es siempre min_hours 0.","tags":["app"],"security":[],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"tiers":{"type":"array","description":"Tramos configurados, de más antelación a menos.","items":{"type":"object","properties":{"min_hours":{"type":"number","description":"Antelación mínima, en horas. Puede llevar decimales: 0.5 = 30 minutos."},"refund_pct":{"type":"number","description":"% del alquiler que se devuelve."}}}},"free_hours":{"type":"integer","description":"Umbral del tramo que devuelve el 100% (derivado)."},"penalty_pct":{"type":"number","description":"Peor caso: 100 menos el % del último tramo (derivado)."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/support-hours":{"get":{"operationId":"appSupportHours","summary":"Horario de atención","description":"Horario de atención al cliente y si en este momento hay alguien.","tags":["app"],"security":[],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"timezone":{"type":"string","examples":["Europe/Madrid"]},"open":{"type":"boolean","description":"Si hay alguien ahora mismo."},"opens_at":{"type":"string","description":"Próxima apertura (HH:mm)."},"closes_at":{"type":"string","description":"Cierre de hoy (HH:mm)."},"hours":{"type":"object","description":"Tramos por tipo de día."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/app/push/ack":{"post":{"operationId":"appPushAck","summary":"Acuse de un aviso push","description":"La app avisa de que el cliente ha TOCADO un aviso. Es lo único que permite medir aperturas de push.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["dedupe_key"],"properties":{"dedupe_key":{"type":"string","description":"Identificador del envío, tal y como viaja en el `data` del push."},"platform":{"type":"string","enum":["android","ios"],"description":"Plataforma del aparato."}}}}}}}},"/api/app/contact/threads/{id}/close":{"post":{"operationId":"appCloseThread","summary":"Cerrar una conversación","description":"Cierra un hilo de soporte del cliente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"thread_id":{"type":"string","format":"uuid"},"status":{"type":"string","examples":["closed"]}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) del hilo.","schema":{"type":"string","format":"uuid"}}]}},"/api/app/contact/threads/{id}/photos":{"post":{"operationId":"appThreadPhotos","summary":"Adjuntar fotos a una conversación","description":"Sube fotos a un hilo de soporte del cliente.","tags":["app"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación correcta.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true],"description":"true cuando la operación salió bien."},"photos":{"type":"array","description":"URLs de las fotos subidas.","items":{"type":"string","format":"uri"}}}}}}},"401":{"description":"Falta el token de cliente o no es válido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de este cliente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"El estado actual no permite la operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Faltan datos o no son válidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error inesperado del servidor. Siempre en JSON, nunca una página HTML.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"description":"Identificador (uuid) del hilo.","schema":{"type":"string","format":"uuid"}}]}},"/api/mcp":{"post":{"operationId":"mcpJsonRpc","summary":"Servidor MCP (JSON-RPC 2.0)","description":"Servidor MCP de solo lectura, sin autenticación: `initialize`, `tools/list` y `tools/call` con las herramientas `listar_flota`, `consultar_precio` e `informacion_empresa`.","tags":["agentes"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"description":"Identificador de la petición (número o cadena)."},"method":{"type":"string","examples":["initialize","tools/list","tools/call"]},"params":{"type":"object","description":"Parámetros del método."}}}}}},"responses":{"200":{"description":"Respuesta JSON-RPC 2.0. Con `Accept: text/event-stream` la misma respuesta viaja como un evento SSE (transporte Streamable HTTP).","content":{"application/json":{"schema":{"type":"object","required":["jsonrpc"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"description":"El mismo identificador que mandó el cliente."},"result":{"type":"object","description":"Resultado del método (capacidades, lista de herramientas o contenido devuelto)."},"error":{"type":"object","description":"Error JSON-RPC.","properties":{"code":{"type":"integer"},"message":{"type":"string"}}}}}},"text/event-stream":{"schema":{"type":"string","description":"Evento `message` con la respuesta JSON-RPC en `data`."}}}},"429":{"description":"Límite de tasa superado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"mcpDescriptor","summary":"Qué es este servidor MCP","description":"Resumen legible del servidor y sus herramientas, sin hablar JSON-RPC.","tags":["agentes"],"security":[],"responses":{"200":{"description":"Descripción del servidor.","content":{"application/json":{"schema":{"type":"object","required":["name","protocolVersion"],"properties":{"name":{"type":"string","examples":["rentacarfurgo"]},"title":{"type":"string"},"version":{"type":"string"},"protocolVersion":{"type":"string","examples":["2025-03-26"]},"transport":{"type":"string","enum":["streamable-http"]},"endpoint":{"type":"string","format":"uri"},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}}}}}}}},"/llms.txt":{"get":{"operationId":"llmsTxt","summary":"Guía para agentes","description":"Cuándo usar este sitio, qué puede y qué no puede hacer aquí un agente, y el índice de páginas. Markdown.","tags":["agentes"],"security":[],"responses":{"200":{"description":"Guía en Markdown (llms.txt).","content":{"text/markdown":{"schema":{"type":"string","description":"Documento Markdown con las secciones «Cuándo usar este sitio», «Para agentes y desarrolladores» y el índice de páginas."}}}}}}}}}