Guías
Primeros pasos
El mismo recorrido que el tutorial «Empieza con la API paso a paso» de Desarrolladores, para quien programa. Crea una clave de pruebas (pt_test_…) con permisos de solo lectura, comprueba la conexión, lee datos reales y, cuando la integración esté lista, pasa a una clave live con los permisos justos. Los avisos automáticos (webhooks) y las escrituras requieren el plan Developer o superior.
1. Crea una clave en «Mis claves»
Elige el entorno de pruebas y solo permisos de lectura (por ejemplo budgets:read). La clave completa se muestra una sola vez: guárdala en un gestor de contraseñas y envíala en la cabecera Authorization: Bearer.
2. Comprueba la conexión
GET /api/v1/ping
Responde con el nombre de la clave, sus permisos y el entorno. Un 401 significa clave no válida; un 403, que falta un permiso.
3. Lee tus datos reales
GET /api/v1/budgets
?limit=3
Los listados se paginan por cursor: repite la llamada con cursor=<next_cursor> hasta que has_more sea false. Cada respuesta trae X-RateLimit-Remaining con las peticiones que quedan en la ventana.
4. Recibe avisos automáticos
POST /api/v1/webhooks
{ "url": "https://tu-sistema.com/avisos-del-taller", "events": ["lead.accepted", "vehicle.ready"] }También se crean desde la pestaña Webhooks sin programar. Guarda el secreto de firma y verifica X-PT-Signature en cada entrega; responde 2xx en menos de 5 segundos.
5. Pasa a producción
Antes, si quieres, prueba escrituras, aceptaciones, facturas y mensajes sin riesgo en el entorno de pruebas (https://demo.programa-taller.es/desarrolladores-sandbox): datos ficticios y nada sale de verdad. Crea una clave live con los permisos mínimos y, si vas a escribir datos, añade Idempotency-Key a cada POST. Los límites y el plan de API del taller se ven en la pestaña Consumo; los planes Developer y Business se contratan desde el propio programa.
Guía: crear un presupuesto y proponer cita
Flujo típico de un CRM o una web de reservas. Todas las peticiones POST llevan Idempotency-Key (un UUID nuevo por operación; repítelo solo al reintentar la misma). Necesitas una clave live con clients:write, vehicles:write, budgets:write, appointments:read y appointments:write.
1. Cliente: crearlo o recuperar el existente
POST /api/v1/clients
{ "name": "Laura Gómez", "phone": "600111222", "email": "laura@ejemplo.com" }Con ?on_conflict=return_existing, si el teléfono, email o NIF ya existen recibes ese cliente (200) en vez de 409.
2. Vehículo del cliente
POST /api/v1/vehicles
{ "client_id": 1204, "plate": "1234KLM", "brand": "Seat", "model": "León" }Si la matrícula es de otro cliente: 409 vehicle_belongs_to_other_client (el taller decide).
3. Presupuesto con sus partidas
POST /api/v1/budgets
{ "client_id": 1204, "vehicle_id": 871, "lines": [{ "description": "Cambio de aceite y filtro", "quantity": 1, "unit_price": 65 }] }La respuesta trae los totales calculados. El cliente acepta y firma desde el enlace que le envía el taller (la API nunca da ese enlace); si te lo confirma a ti por otra vía, acéptalo en su nombre con POST /budgets/{id}/accept (permiso budgets:accept).
4. Huecos libres
GET /api/v1/appointments/availability
?from=2026-10-14&to=2026-10-18&duration_minutes=60
5. Proponer la cita
POST /api/v1/appointments
{ "budget_id": 1234, "start": "2026-10-14T09:00:00+02:00", "duration_minutes": 60 }En modo «proponer» queda status=proposed hasta que el taller la confirma (recibirás appointment.confirmed por webhook). Si el hueco se ha ocupado: 409 slot_unavailable con alternativas.
Guía: tu bot o IA en los canales del taller
Tu asistente contesta por WhatsApp, SMS, chat web o email en lugar del asistente del programa. Necesitas una clave live con communications:read, messages:send, settings:write y webhooks:manage (plan Business o Partner). Pruébalo antes en el entorno de pruebas con POST /sandbox/simulate/message.
1. Suscríbete a message.received
POST /api/v1/webhooks
{ "url": "https://tu-bot.com/taller", "events": ["message.received"] }Cada mensaje del cliente llega firmado con la clave de la conversación (key), el cliente, el presupuesto activo, el texto y los adjuntos.
2. Activa el modo asistente externo
PUT /api/v1/settings/assistant
{ "external_channels": ["whatsapp", "chat"], "fallback_minutes": 10 }En esos canales el asistente del programa deja de contestar. Si no respondes a tiempo, o tu clave o tu webhook dejan de funcionar, vuelve a contestar él a ese mensaje.
3. Lee el hilo si lo necesitas
GET /api/v1/conversations/{key}/messages
4. Responde
POST /api/v1/conversations/{key}/messages
{ "text": "Mañana a las 9:00 tenemos hueco. ¿Se la reservo?" }Fuera de la ventana de 24 h de WhatsApp envía template_id (GET /messages/templates). Para cerrar la venta: POST /appointments, POST /budgets/{id}/accept o POST /budgets/{id}/invoice según tus permisos.
Guía: flotas y renting con una clave de cliente
Para que una flota, una empresa de renting o un cliente con muchos vehículos conecte sus sistemas con el taller sin ver los datos de los demás clientes, el taller le crea una clave limitada a su ficha de cliente («Limitar a un cliente» al crearla en Mis claves). Con ella cada listado devuelve solo sus datos, un id de otro cliente responde 404, las operaciones de todo el taller responden 403 client_key_forbidden y sus webhooks solo reciben sus eventos.
1. Comprueba a qué cliente está limitada
GET /api/v1/ping
key.client trae el id y el nombre del cliente; en una clave del taller es null.
2. Sus vehículos
GET /api/v1/vehicles
?limit=200
Solo salen los vehículos de su ficha. Para dar de alta uno nuevo, POST /vehicles con su propio client_id.
3. Pedir un presupuesto
POST /api/v1/budgets
{ "client_id": 1204, "vehicle_id": 871, "public_notes": "Revisión de los 60.000 km" }client_id debe ser el de la clave (si no, 403 client_key_forbidden). El taller lo completa y lo envía; la flota lo acepta o rechaza con POST /budgets/{id}/accept o /reject (permiso budgets:accept).
4. Proponer cita y seguir el estado
POST /api/v1/appointments
{ "budget_id": 1234, "start": "2026-10-20T09:00:00+02:00", "duration_minutes": 60 }Suscribe un webhook a lead.status_changed, vehicle.ready e invoice.issued para enterarte al momento; solo llegarán los de sus vehículos.
Entorno de pruebas
https://demo.programa-taller.es es una instancia con datos ficticios que se restauran cada 2 horas. Toda clave allí es de pruebas con todos los permisos: puede escribir, aceptar, facturar y «enviar», pero nada sale de verdad (los envíos se registran como simulados) y los webhooks se emiten igual para probar el ciclo completo. Tus claves, webhooks y registro de peticiones se conservan entre reinicios.
1. Pide tu clave
POST /api/v1/sandbox/keys
{ "email": "dev@ejemplo.com", "company": "Integraciones Ejemplo SL", "name": "Ana Pérez" }También desde la página https://demo.programa-taller.es/desarrolladores-sandbox. La clave se muestra una sola vez y vale 30 días.
2. Simula un cliente que escribe
POST /api/v1/sandbox/simulate/message
{ "channel": "whatsapp", "from": "+34600111222", "text": "¿Tienen hueco mañana?" }3. Cambia a producción
Cambia la URL base por la de la instancia del taller y usa una clave live creada en su sección Desarrolladores.
Clientes
GET /api/v1/clients clients:read
Listar clientes
Ordenados por id ascendente. Los clientes borrados por RGPD aparecen anonimizados, con erased_at informado.
Permiso necesario: clients:read.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
search | string | No | Busca en nombre, teléfono, email y NIF/CIF (mínimo 2 caracteres). |
expand | string [vehicles] | No | Relaciones opcionales a incluir. |
limit | integer · default 50 | No | Resultados por página (1–200). |
cursor | string | No | Cursor opaco devuelto en next_cursor de la página anterior. |
Ejemplo
curl "https://tu-taller.example/api/v1/clients" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": 1204,
"name": "Laura Gómez",
"phone": "+34600111222",
"phone_secondary": null,
"whatsapp_phone": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"vip": false,
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
},
"erased_at": null,
"vehicles": [
{
"id": 871,
"client_id": 1204,
"client": null,
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
]
}
],
"next_cursor": "aWQ6MTIzNA",
"has_more": true
}
POST /api/v1/clients clients:write
Crear un cliente
Nunca fusiona con una ficha existente: si el NIF/CIF, el email o el teléfono ya están en otro cliente responde 409 client_exists con existing_id (o 200 con ese cliente si pasas ?on_conflict=return_existing). El teléfono se guarda como en la ficha (España en 9 dígitos, otros países con prefijo) y el email en minúsculas; no se corrigen erratas. Las bajas comerciales quedan en el registro de consentimientos.
Permiso necesario: clients:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
on_conflict | string [error, return_existing] · default error | No | error (por defecto): 409 si ya existe. return_existing: 200 con el cliente existente. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"name": "Laura Gómez",
"phone": "600111222",
"phone_secondary": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
}
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/clients" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Laura Gómez","phone":"600111222","phone_secondary":null,"email":"laura@ejemplo.com","billing_email":null,"tax_id":"12345678Z","address":"C/ Mayor 12","city":"Barcelona","zip":"08001","province":"Barcelona","country":"ES","preferred_language":"es","preferred_contact_method":"whatsapp","marketing_opt_out":false,"channel_opt_out":{"email":false,"sms":false,"whatsapp":false,"call":false}}'
Respuesta
{
"id": 1204,
"name": "Laura Gómez",
"phone": "+34600111222",
"phone_secondary": null,
"whatsapp_phone": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"vip": false,
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
},
"erased_at": null,
"vehicles": [
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
]
}
GET /api/v1/clients/{id} clients:read
Detalle de un cliente
Permiso necesario: clients:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del cliente. |
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
expand | string [vehicles] | No | Relaciones opcionales a incluir. |
Ejemplo
curl "https://tu-taller.example/api/v1/clients/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": 1204,
"name": "Laura Gómez",
"phone": "+34600111222",
"phone_secondary": null,
"whatsapp_phone": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"vip": false,
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
},
"erased_at": null,
"vehicles": [
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
]
}
PATCH /api/v1/clients/{id} clients:write
Modificar un cliente
Solo cambian los campos enviados (null vacía el campo). Cambiar email o teléfono está permitido y deja el valor anterior → nuevo en la actividad del cliente con el nombre de la clave; emite client.updated. No se puede poner el email, teléfono o NIF de OTRA ficha (409 client_exists). Las fichas borradas por RGPD responden 409 client_erased.
Permiso necesario: clients:write.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del cliente. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | No | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"name": "Laura Gómez Ruiz",
"phone": "600111222",
"phone_secondary": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
}
}
Ejemplo
curl -X PATCH "https://tu-taller.example/api/v1/clients/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Laura Gómez Ruiz","phone":"600111222","phone_secondary":null,"email":"laura@ejemplo.com","billing_email":null,"tax_id":"12345678Z","address":"C/ Mayor 12","city":"Barcelona","zip":"08001","province":"Barcelona","country":"ES","preferred_language":"es","preferred_contact_method":"whatsapp","marketing_opt_out":false,"channel_opt_out":{"email":false,"sms":false,"whatsapp":false,"call":false}}'
Respuesta
{
"id": 1204,
"name": "Laura Gómez",
"phone": "+34600111222",
"phone_secondary": null,
"whatsapp_phone": null,
"email": "laura@ejemplo.com",
"billing_email": null,
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona",
"country": "ES",
"preferred_language": "es",
"preferred_contact_method": "whatsapp",
"vip": false,
"marketing_opt_out": false,
"channel_opt_out": {
"email": false,
"sms": false,
"whatsapp": false,
"call": false
},
"erased_at": null,
"vehicles": [
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
]
}
Vehículos
GET /api/v1/vehicles vehicles:read
Listar vehículos
Incluye el último kilometraje anotado y el vencimiento de la ITV.
Permiso necesario: vehicles:read.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
plate | string | No | Matrícula exacta (se ignoran espacios y guiones). |
client_id | integer | No | Filtrar por cliente. |
status | string [activo, baja_temporal, baja] | No | Estado del vehículo. |
limit | integer · default 50 | No | Resultados por página (1–200). |
cursor | string | No | Cursor opaco devuelto en next_cursor de la página anterior. |
Ejemplo
curl "https://tu-taller.example/api/v1/vehicles" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
],
"next_cursor": "aWQ6MTIzNA",
"has_more": true
}
POST /api/v1/vehicles vehicles:write
Dar de alta un vehículo
Siempre a nombre de un cliente existente (client_id). La matrícula se normaliza (mayúsculas, sin espacios ni guiones). Si ya está en la ficha de OTRO cliente: 409 vehicle_belongs_to_other_client. Si el mismo cliente ya la tiene: 409 vehicle_exists con existing_id (o 200 con ?on_conflict=return_existing).
Permiso necesario: vehicles:write.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
on_conflict | string [error, return_existing] · default error | No | error (por defecto) o return_existing. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"client_id": 1204,
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/vehicles" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"client_id":1204,"plate":"1234 KLM","vin":"VSSZZZ5FZJR123456","brand":"Seat","model":"León","variant":"1.5 TSI","year":2019,"registration_date":"2019-03-15","fuel":"Gasolina","transmission":"Manual","engine_code":"DADA","horsepower":130,"displacement":"1498","color_code":null,"environmental_label":"C","itv_expiry_date":"2027-03-15","status":"activo"}'
Respuesta
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
GET /api/v1/vehicles/{id} vehicles:read
Detalle de un vehículo
Permiso necesario: vehicles:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del vehículo. |
Ejemplo
curl "https://tu-taller.example/api/v1/vehicles/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
PATCH /api/v1/vehicles/{id} vehicles:write
Modificar un vehículo
Cambios parciales. client_id no admite null: un vehículo nunca se desvincula de su titular por API (sí puede pasar a otro cliente existente).
Permiso necesario: vehicles:write.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del vehículo. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | No | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"client_id": 1204,
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
Ejemplo
curl -X PATCH "https://tu-taller.example/api/v1/vehicles/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"client_id":1204,"plate":"1234 KLM","vin":"VSSZZZ5FZJR123456","brand":"Seat","model":"León","variant":"1.5 TSI","year":2019,"registration_date":"2019-03-15","fuel":"Gasolina","transmission":"Manual","engine_code":"DADA","horsepower":130,"displacement":"1498","color_code":null,"environmental_label":"C","itv_expiry_date":"2027-03-15","status":"activo"}'
Respuesta
{
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
}
GET /api/v1/vehicles/{id}/history vehicles:read
Historial del vehículo
Visitas al taller (todos sus presupuestos salvo los de uso interno, del más reciente al más antiguo) con estado, entrada y salida, kilometraje, importe, nº de factura y conceptos de las partidas; más las intervenciones anotadas en otros talleres. Nunca incluye costes.
Permiso necesario: vehicles:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del vehículo. |
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
limit | integer · default 50 | No | Máximo de visitas (1–200). |
Ejemplo
curl "https://tu-taller.example/api/v1/vehicles/1234/history" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"vehicle": {
"id": 871,
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"plate": "1234 KLM",
"vin": "VSSZZZ5FZJR123456",
"brand": "Seat",
"model": "León",
"variant": "1.5 TSI",
"year": 2019,
"registration_date": "2019-03-15",
"fuel": "Gasolina",
"transmission": "Manual",
"engine_code": "DADA",
"horsepower": 130,
"displacement": "1498",
"color_code": null,
"environmental_label": "C",
"km": 84500,
"itv_expiry_date": "2027-03-15",
"status": "activo"
},
"visits": [
{
"budget_id": 1234,
"status": "Facturado",
"created_at": "2026-10-06T09:30:00.000Z",
"checked_in_at": "2026-09-02T07:30:00.000Z",
"delivered_at": "2026-09-03T16:00:00.000Z",
"km": 84210,
"amount": 245.3,
"invoice": "F26/118",
"category": "Mantenimiento",
"works": [
"Cambio de aceite y filtro"
]
}
],
"external_interventions": [
{
"title": "Cambio de neumáticos",
"date": "2025-11-20",
"km": 78000,
"workshop_name": null,
"warranty_until": null
}
]
}
Presupuestos
GET /api/v1/budgets budgets:read
Listar presupuestos
Nunca incluye los presupuestos de uso interno del taller ni datos de coste. El detalle (con partidas y totales) está en /budgets/{id}.
Permiso necesario: budgets:read.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
status | string | No | Estado exacto (Pendiente, Enviado, Aprobado, Finalizado, Facturado, Rechazado…). |
client_id | integer | No | Filtrar por cliente. |
vehicle_id | integer | No | Filtrar por vehículo. |
from | string (date-time) | No | Creados a partir de esta fecha. |
to | string (date-time) | No | Creados hasta esta fecha. |
updated_since | string (date-time) | No | Solo registros modificados a partir de esta fecha (ISO 8601). |
limit | integer · default 50 | No | Resultados por página (1–200). |
cursor | string | No | Cursor opaco devuelto en next_cursor de la página anterior. |
Ejemplo
curl "https://tu-taller.example/api/v1/budgets" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": null
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null
}
],
"next_cursor": "aWQ6MTIzNA",
"has_more": true
}
POST /api/v1/budgets budgets:write
Crear un presupuesto
Entra en «Pendiente» con canal «API», igual que un alta desde el programa (registro, hito de apertura, webhook lead.created). Los totales se calculan en el servidor con el impuesto del taller; si una partida no trae tax_rate usa el del taller. El vehículo, si se indica, debe ser del cliente. No admite categorías de uso interno. Con notify_client=true (y permiso communications:send) se envía al cliente el acuse con su enlace de seguimiento.
Permiso necesario: budgets:write.
Clave de cliente: solo devuelve o crea datos de su cliente.
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"client_id": 1204,
"vehicle_id": 871,
"category_id": 5,
"subcategory_id": 51,
"client_reference": "PED-2026-118",
"public_notes": "Revisar también el ruido de la suspensión.",
"lines": [
{
"description": "Cambio de aceite y filtro",
"quantity": 1,
"unit_price": 65,
"tax_rate": 21,
"discount_pct": 0,
"line_type": "labor",
"reference": null,
"group_title": null
}
],
"notify_client": false
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"client_id":1204,"vehicle_id":871,"category_id":5,"subcategory_id":51,"client_reference":"PED-2026-118","public_notes":"Revisar también el ruido de la suspensión.","lines":[{"description":"Cambio de aceite y filtro","quantity":1,"unit_price":65,"tax_rate":21,"discount_pct":0,"line_type":"labor","reference":null,"group_title":null}],"notify_client":false}'
Respuesta
{
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": 2,
"name": "Elevador 2"
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
GET /api/v1/budgets/{id} budgets:read
Detalle de un presupuesto
Partidas (descripción, cantidad, precio unitario, impuesto, tipo, descuento), totales con desglose, cita, fechas de entrada y salida, responsable y URL pública de seguimiento.
Permiso necesario: budgets:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Ejemplo
curl "https://tu-taller.example/api/v1/budgets/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": 2,
"name": "Elevador 2"
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
GET /api/v1/budgets/{id}/activity budgets:read
Historial del presupuesto
Lo que ha pasado en el presupuesto, de más reciente a más antiguo: cambios de estado, envíos, aceptación (con su origen), citas, mensajes del cliente, facturación… El mismo registro que ve el taller en la ficha; public=true marca lo que también ve el cliente. Los códigos de un solo uso y enlaces de acceso salen ocultos.
Permiso necesario: budgets:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
visibility | string [all, public] · default all | No | all: todo el registro; public: solo lo que ve el cliente. |
limit | integer · default 50 | No | Resultados por página (1–200). |
cursor | string | No | Cursor opaco devuelto en next_cursor de la página anterior. |
Ejemplo
curl "https://tu-taller.example/api/v1/budgets/1234/activity" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": 88123,
"at": "2026-10-06T09:30:00.000Z",
"event": "web_accepted",
"detail": "Presupuesto aceptado por el cliente desde la web de seguimiento",
"source": "client",
"public": true,
"user": {
"id": 3,
"name": "Marta"
}
}
],
"next_cursor": "aWQ6MTIzNA",
"has_more": true
}
POST /api/v1/budgets/{id}/lines budgets:write
Añadir una partida
Las demás partidas conservan su id. Deja instantánea previa y registro como cualquier edición del programa. 409 budget_locked si el presupuesto está Facturado, Facturado externamente, Cancelado, Rechazado, Desistido o ya tiene factura.
Permiso necesario: budgets:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"description": "Cambio de aceite y filtro",
"quantity": 1,
"unit_price": 65,
"tax_rate": 21,
"discount_pct": 0,
"line_type": "labor",
"reference": null,
"group_title": null
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/lines" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"description":"Cambio de aceite y filtro","quantity":1,"unit_price":65,"tax_rate":21,"discount_pct":0,"line_type":"labor","reference":null,"group_title":null}'
Respuesta
{
"line": {
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
},
"budget": {
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": null,
"name": null
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
}
PATCH /api/v1/budgets/{id}/lines/{lineId} budgets:write
Modificar una partida
Permiso necesario: budgets:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
lineId | integer | Sí | Id de la partida. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | No | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"description": "Cambio de aceite y filtro",
"quantity": 1,
"unit_price": 65,
"tax_rate": 21,
"discount_pct": 0,
"line_type": "labor",
"reference": null,
"group_title": null
}
Ejemplo
curl -X PATCH "https://tu-taller.example/api/v1/budgets/1234/lines/5501" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"description":"Cambio de aceite y filtro","quantity":1,"unit_price":65,"tax_rate":21,"discount_pct":0,"line_type":"labor","reference":null,"group_title":null}'
Respuesta
{
"line": {
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
},
"budget": {
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": null,
"name": null
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
}
DELETE /api/v1/budgets/{id}/lines/{lineId} budgets:write
Quitar una partida
Devuelve el presupuesto con los totales recalculados. La partida queda en la instantánea previa del historial de versiones.
Permiso necesario: budgets:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
lineId | integer | Sí | Id de la partida. |
Ejemplo
curl -X DELETE "https://tu-taller.example/api/v1/budgets/1234/lines/5501" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": 2,
"name": "Elevador 2"
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
POST /api/v1/budgets/{id}/status budgets:write
Cambiar el estado
Admite Pendiente/En cotización/Enviado (antes de la aceptación), En curso (presupuesto ya aceptado), Finalizado (desde Aprobado, En curso o En espera) y Cancelado. «Aprobado» responde 403 client_acceptance_required: la aceptación la firma el cliente desde el enlace que le envía el taller, o la registra tu integración en su nombre con POST /budgets/{id}/accept (permiso budgets:accept). Rechazar va por POST /budgets/{id}/reject y facturar por POST /budgets/{id}/invoice (permiso invoices:write); aquí responden 403 status_transition_forbidden, igual que desistir; desde Facturado o Cancelado, 409 budget_locked. «Enviado» exige además communications:send y sent_via: registra que TU sistema ya lo envió (no lo envía). Ningún cambio avisa al cliente salvo Finalizado con notify_client=true y communications:send.
Permiso necesario: budgets:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"status": "En curso",
"sent_via": "email",
"notify_client": false
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/status" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"status":"En curso","sent_via":"email","notify_client":false}'
Respuesta
{
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": {
"id": 2,
"name": "Elevador 2"
}
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
}
POST /api/v1/budgets/{id}/accept budgets:accept
Aceptar en nombre del cliente
Para cuando el cliente acepta por otra vía (teléfono, en persona, tu web…). Obligatoria la evidencia: accepted_by y channel (phone, in_person, email, whatsapp, other); note es opcional. Pasa por la misma regla de aceptación del programa: estado Aprobado, transición, datos fiscales mínimos del cliente (si faltan, 422 fiscal_data_incomplete con missing), descuento de campaña, aviso interno al taller, confirmación al cliente según los avisos configurados y webhooks lead.accepted y budget.accepted. Solo desde «Enviado» (si ya está aceptado responde 200 con already=true); Cancelado, Rechazado, Desistido, Finalizado o Facturado responden 409 budget_locked. Si el taller exige firma, la API acepta igualmente y lo deja escrito: «sin firma, aceptado por integración» (signature=waived_by_integration). En el historial queda «Aceptado por API en nombre del cliente · integración <clave> · <accepted_by> por <canal>».
Permiso necesario: budgets:accept.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"accepted_by": "Laura Gómez",
"channel": "phone",
"note": "Confirmado por teléfono a las 10:12"
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/accept" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"accepted_by":"Laura Gómez","channel":"phone","note":"Confirmado por teléfono a las 10:12"}'
Respuesta
{
"budget": {
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": null
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
},
"already": false,
"signature": "not_required"
}
POST /api/v1/budgets/{id}/reject budgets:accept
Rechazar en nombre del cliente
Con motivo obligatorio. Misma regla que el rechazo del cliente: desde Pendiente, En cotización, Enviado o Aprobado (si el vehículo aún no ha entrado). Emite lead.rejected y budget.rejected y queda en el historial con el nombre de la clave.
Permiso necesario: budgets:accept.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"reason": "Lo hará en otro taller",
"rejected_by": "Laura Gómez",
"channel": "phone"
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/reject" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason":"Lo hará en otro taller","rejected_by":"Laura Gómez","channel":"phone"}'
Respuesta
{
"budget": {
"id": 1234,
"status": "Aprobado",
"created_at": "2026-10-06T09:30:00.000Z",
"updated_at": "2026-10-06T09:30:00.000Z",
"channel": "web",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"subcategory": {
"id": 5,
"name": "Mantenimiento"
},
"assigned_user": {
"id": 3,
"name": "Marta"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"appointment": {
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"status": "confirmed",
"client_confirmed_at": null,
"estimated_duration_minutes": 60,
"box": null
},
"date_in": null,
"date_out": null,
"km": 84500,
"client_reference": null,
"waiting_parts": false,
"on_hold": false,
"hold_until": null,
"client_signed_at": null,
"estimated_ready_at": null,
"delivered_at": null,
"reject_reason": null,
"public_notes": "Revisar también el ruido en la suspensión.",
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 5501,
"description": "Cambio de aceite y filtro",
"extended_detail": null,
"reference": null,
"line_type": "labor",
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"price_estimated": false,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
]
},
"already": false,
"signature": "not_required"
}
POST /api/v1/budgets/{id}/documents budgets:write
Adjuntar un documento
multipart/form-data con el campo «file» (JPEG, PNG, WebP o PDF, comprobado por su contenido; máximo 4 MB). Por defecto solo lo ve el taller; client_visible=true lo enseña en el enlace de seguimiento y mechanic_visible=true en la app del mecánico. La huella de idempotencia incluye el fichero.
Permiso necesario: budgets:write.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo (multipart/form-data)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
file | string (binary) | Sí | Fichero |
client_visible | string [true, false] | No | |
mechanic_visible | string [true, false] | No | |
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/documents" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@fichero.pdf" \
-F "client_visible=false" \
-F "mechanic_visible=false"
Respuesta
{
"id": 991,
"budget_id": 1234,
"filename": "parte-de-trabajo.pdf",
"content_type": "application/pdf",
"size": 182044,
"url": "https://…/leads/1234/api-parte-de-trabajo.pdf",
"client_visible": false,
"mechanic_visible": false,
"created_at": "2026-10-06T09:30:00.000Z"
}
Facturas
POST /api/v1/budgets/{id}/invoice invoices:write
Facturar un presupuesto
Usa la misma facturación del programa: copia las partidas y el impuesto del presupuesto, crea el asiento contable y, con issue=true (por defecto), emite la factura con la serie que toque, número correlativo, hash encadenado y QR Verifactu. El presupuesto pasa a Facturado. Se factura uno Aprobado, En curso, En espera o Finalizado con importe; si ya tiene factura se devuelve esa (created=false), nunca una segunda. Emite invoice.created e invoice.issued. Las facturas rectificativas no están disponibles por API: se hacen desde el programa.
Permiso necesario: invoices:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id del presupuesto. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"issue": true,
"pay_method": "TARJETA"
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/budgets/1234/invoice" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"issue":true,"pay_method":"TARJETA"}'
Respuesta
{
"invoice": {
"id": 412,
"number": 87,
"series": "F26",
"full_number": "F2687",
"kind": "invoice",
"rectifies_number": null,
"status": "issued",
"date": "2026-10-06",
"issued_at": "2026-10-06T09:30:00.000Z",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"budget_id": 1234,
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"plate": "1234 KLM",
"km": 84500,
"total": 78.65,
"currency": "EUR",
"payments": {
"paid": 78.65,
"pending": 0,
"settled": true
},
"payment_method": "Tarjeta",
"rebu": false,
"verifactu_hash": "3f9a…",
"registered_externally": false,
"billing": {
"name": "Laura Gómez",
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona"
},
"date_in": null,
"date_out": null,
"public_notes": null,
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 9001,
"description": "Cambio de aceite y filtro",
"reference": null,
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
],
"payment_list": [
{
"id": 77,
"date": "2026-10-06T09:30:00.000Z",
"amount": 78.65,
"currency": "EUR",
"method": "Tarjeta"
}
]
},
"created": true
}
GET /api/v1/invoices invoices:read
Listar facturas
Facturas emitidas y borradores, ordenadas por id. Incluye el resumen de cobros.
Permiso necesario: invoices:read.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
from | string (date-time) | No | Fecha de factura desde. |
to | string (date-time) | No | Fecha de factura hasta. |
client_id | integer | No | Filtrar por cliente. |
status | string [draft, issued, cancelled] | No | Estado de la factura. |
series | string | No | Serie exacta. |
limit | integer · default 50 | No | Resultados por página (1–200). |
cursor | string | No | Cursor opaco devuelto en next_cursor de la página anterior. |
Ejemplo
curl "https://tu-taller.example/api/v1/invoices" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": 412,
"number": 87,
"series": "F26",
"full_number": "F2687",
"kind": "invoice",
"rectifies_number": null,
"status": "issued",
"date": "2026-10-06",
"issued_at": "2026-10-06T09:30:00.000Z",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"budget_id": 1234,
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"plate": "1234 KLM",
"km": 84500,
"total": 78.65,
"currency": "EUR",
"payments": {
"paid": 78.65,
"pending": 0,
"settled": true
},
"payment_method": "Tarjeta",
"rebu": false,
"verifactu_hash": "3f9a…",
"registered_externally": false
}
],
"next_cursor": "aWQ6MTIzNA",
"has_more": true
}
GET /api/v1/invoices/{id} invoices:read
Detalle de una factura
Permiso necesario: invoices:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id de la factura. |
Ejemplo
curl "https://tu-taller.example/api/v1/invoices/1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": 412,
"number": 87,
"series": "F26",
"full_number": "F2687",
"kind": "invoice",
"rectifies_number": null,
"status": "issued",
"date": "2026-10-06",
"issued_at": "2026-10-06T09:30:00.000Z",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"budget_id": 1234,
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"plate": "1234 KLM",
"km": 84500,
"total": 78.65,
"currency": "EUR",
"payments": {
"paid": 78.65,
"pending": 0,
"settled": true
},
"payment_method": "Tarjeta",
"rebu": false,
"verifactu_hash": "3f9a…",
"registered_externally": false,
"billing": {
"name": "Laura Gómez",
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona"
},
"date_in": null,
"date_out": null,
"public_notes": null,
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 9001,
"description": "Cambio de aceite y filtro",
"reference": null,
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
],
"payment_list": [
{
"id": 77,
"date": "2026-10-06T09:30:00.000Z",
"amount": 78.65,
"currency": "EUR",
"method": "Tarjeta"
}
]
}
GET /api/v1/invoices/{id}/pdf invoices:read
PDF de una factura
El mismo PDF que genera el programa (con QR Verifactu si la factura está emitida). Respuesta application/pdf.
Permiso necesario: invoices:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id de la factura. |
Ejemplo
curl "https://tu-taller.example/api/v1/invoices/1234/pdf" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
(application/pdf)
POST /api/v1/invoices/{id}/payments invoices:write
Anotar un cobro
Mismo registro de cobros que la ficha de la factura: la marca «pagada» se recalcula con todos los cobros (nunca se fuerza) y se emite payment.received. Solo sobre facturas emitidas. Devuelve la factura con su resumen de cobros.
Permiso necesario: invoices:write.
Clave de cliente: no disponible (403 client_key_forbidden).
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | integer | Sí | Id de la factura. |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"amount": 245.3,
"date": "2026-10-13",
"pay_method": "TARJETA",
"notes": "Cobrado en mostrador"
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/invoices/1234/payments" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount":245.3,"date":"2026-10-13","pay_method":"TARJETA","notes":"Cobrado en mostrador"}'
Respuesta
{
"id": 412,
"number": 87,
"series": "F26",
"full_number": "F2687",
"kind": "invoice",
"rectifies_number": null,
"status": "issued",
"date": "2026-10-06",
"issued_at": "2026-10-06T09:30:00.000Z",
"client_id": 1204,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"budget_id": 1234,
"vehicle_id": 871,
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"plate": "1234 KLM",
"km": 84500,
"total": 78.65,
"currency": "EUR",
"payments": {
"paid": 78.65,
"pending": 0,
"settled": true
},
"payment_method": "Tarjeta",
"rebu": false,
"verifactu_hash": "3f9a…",
"registered_externally": false,
"billing": {
"name": "Laura Gómez",
"tax_id": "12345678Z",
"address": "C/ Mayor 12",
"city": "Barcelona",
"zip": "08001",
"province": "Barcelona"
},
"date_in": null,
"date_out": null,
"public_notes": null,
"totals": {
"base": 65,
"tax": 13.65,
"total": 78.65,
"currency": "EUR",
"tax_rates": [
{
"rate": 21,
"base": 65,
"quota": 13.65
}
]
},
"lines": [
{
"id": 9001,
"description": "Cambio de aceite y filtro",
"reference": null,
"group_title": null,
"quantity": 1,
"unit_price": 65,
"discount_pct": 0,
"tax_rate": 21,
"tax_exempt_code": null,
"base": 65,
"currency": "EUR",
"sort_order": 0
}
],
"payment_list": [
{
"id": 77,
"date": "2026-10-06T09:30:00.000Z",
"amount": 78.65,
"currency": "EUR",
"method": "Tarjeta"
}
]
}
Citas
GET /api/v1/appointments appointments:read
Citas confirmadas y propuestas
Por defecto los próximos 30 días. status=confirmed son citas fijadas en la agenda; status=proposed son propuestas del cliente pendientes de que el taller confirme (bloquean el hueco).
Permiso necesario: appointments:read.
Clave de cliente: solo devuelve o crea datos de su cliente.
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
from | string (date-time) | No | Inicio del rango (por defecto ahora). |
to | string (date-time) | No | Fin del rango (por defecto +30 días, máximo 1 año). |
status | string [confirmed, proposed] | No | Solo un tipo. |
box_id | integer | No | Filtrar por elevador/box. |
Ejemplo
curl "https://tu-taller.example/api/v1/appointments" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"id": "b1234",
"budget_id": 1234,
"status": "confirmed",
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"estimated_duration_minutes": 60,
"client_confirmed_at": null,
"budget_status": "Aprobado",
"checked_in_at": null,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"box": {
"id": 2,
"name": "Elevador 2"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"channel": null,
"proposed_at": null
}
],
"from": "2026-10-06T00:00:00.000Z",
"to": "2026-11-05T00:00:00.000Z"
}
POST /api/v1/appointments appointments:write
Proponer o reservar una cita
Sigue el modo de reserva del taller (booking_mode en GET /workshop). En «propose» se crea una propuesta (status=proposed) que bloquea el hueco hasta que el taller la confirma; pedir mode=book responde 403 booking_mode_propose_only. En «book» la cita queda en firme (status=confirmed), salvo que pidas mode=propose. El hueco se valida con la misma lógica que /appointments/availability; si no está libre, 409 slot_unavailable con hasta 3 alternativas en error.alternatives. No se avisa al cliente salvo notify_client=true con communications:send.
Permiso necesario: appointments:write.
Clave de cliente: solo devuelve o crea datos de su cliente.
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | Sí | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"budget_id": 1234,
"start": "2026-10-14T09:00:00+02:00",
"box_id": 2,
"duration_minutes": 60,
"mode": "propose",
"notify_client": false
}
Ejemplo
curl -X POST "https://tu-taller.example/api/v1/appointments" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"budget_id":1234,"start":"2026-10-14T09:00:00+02:00","box_id":2,"duration_minutes":60,"mode":"propose","notify_client":false}'
Respuesta
{
"id": "b1234",
"budget_id": 1234,
"status": "confirmed",
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"estimated_duration_minutes": 60,
"client_confirmed_at": null,
"budget_status": "Aprobado",
"checked_in_at": null,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"box": {
"id": 2,
"name": "Elevador 2"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"channel": null,
"proposed_at": null
}
GET /api/v1/appointments/availability appointments:read
Huecos libres
Misma lógica que la agenda y la web de seguimiento: horario del taller y de cada box, comida, festivos nacionales, autonómicos y locales, citas abiertas y propuestas pendientes (que bloquean su hueco). Inicios cada 30 min y al menos 1 h desde ahora. Por defecto los próximos 7 días (máximo 31). Incluye booking_mode del taller.
Permiso necesario: appointments:read.
Clave de cliente: permitido (no lleva datos de otros clientes).
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
from | string (date-time) | No | Desde (por defecto ahora). |
to | string (date-time) | No | Hasta (por defecto +7 días; máximo 31 días). |
duration_minutes | integer | No | Duración de la cita (15–720). Por defecto la mínima de la agenda. |
box_id | integer | No | Solo ese box. |
limit | integer · default 100 | No | Máximo de huecos (1–500). |
Ejemplo
curl "https://tu-taller.example/api/v1/appointments/availability" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"data": [
{
"start": "2026-10-14T07:00:00.000Z",
"end": "2026-10-14T08:00:00.000Z",
"box": {
"id": 2,
"name": "Elevador 2"
}
}
],
"duration_minutes": 60,
"from": "2026-10-14T00:00:00.000Z",
"to": "2026-10-21T00:00:00.000Z",
"booking_mode": "propose"
}
DELETE /api/v1/appointments/{id} appointments:write
Anular una cita o retirar una propuesta
b<presupuesto>: anula la cita confirmada (igual que «Cancelar cita» en la ficha). p<propuesta>: retira la propuesta pendiente; con notify_client=true y communications:send se invita al cliente a elegir otra hora. Emite appointment.cancelled.
Permiso necesario: appointments:write.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | string | Sí | Id de la cita (b1234 o p88). |
Consulta (query)
| Nombre | Tipo | Obligatorio | Descripción |
|---|
notify_client | boolean · default false | No | Solo propuestas: avisar al cliente. |
Ejemplo
curl -X DELETE "https://tu-taller.example/api/v1/appointments/b1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"ok": true,
"id": "b1234",
"status": "cancelled"
}
GET /api/v1/appointments/{id} appointments:read
Detalle de una cita
Permiso necesario: appointments:read.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | string | Sí | Id de la cita (b1234 o p88). |
Ejemplo
curl "https://tu-taller.example/api/v1/appointments/b1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Respuesta
{
"id": "b1234",
"budget_id": 1234,
"status": "confirmed",
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"estimated_duration_minutes": 60,
"client_confirmed_at": null,
"budget_status": "Aprobado",
"checked_in_at": null,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"box": {
"id": 2,
"name": "Elevador 2"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"channel": null,
"proposed_at": null
}
PATCH /api/v1/appointments/{id} appointments:write
Mover una cita o cambiar una propuesta
b<presupuesto>: mueve la cita en firme a la nueva hora (solo si el taller trabaja en modo «reservar»; en modo «proponer» responde 403 booking_mode_propose_only: crea una propuesta con POST /appointments). p<propuesta>: retira la propuesta y crea otra a la nueva hora (cambia el id). El hueco se valida igual que al crear (409 slot_unavailable con alternativas). Emite appointment.updated.
Permiso necesario: appointments:write.
Clave de cliente: solo si el recurso es de su cliente; si no, 404.
Parámetros
| Nombre | Tipo | Obligatorio | Descripción |
|---|
id | string | Sí | Id de la cita (b1234 o p88). |
Cabeceras
| Nombre | Tipo | Obligatorio | Descripción |
|---|
Idempotency-Key | string | No | Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict. |
Cuerpo
{
"start": "2026-10-15T10:00:00+02:00",
"box_id": 2,
"duration_minutes": 60,
"notify_client": false
}
Ejemplo
curl -X PATCH "https://tu-taller.example/api/v1/appointments/b1234" \
-H "Authorization: Bearer pt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"start":"2026-10-15T10:00:00+02:00","box_id":2,"duration_minutes":60,"notify_client":false}'
Respuesta
{
"id": "b1234",
"budget_id": 1234,
"status": "confirmed",
"start": "2026-10-06T09:30:00.000Z",
"end": "2026-10-06T10:30:00.000Z",
"estimated_duration_minutes": 60,
"client_confirmed_at": null,
"budget_status": "Aprobado",
"checked_in_at": null,
"client": {
"id": 1204,
"name": "Laura Gómez"
},
"vehicle": {
"id": 871,
"plate": "1234 KLM",
"brand": "Seat",
"model": "León"
},
"mechanic": {
"id": 3,
"name": "Marta"
},
"box": {
"id": 2,
"name": "Elevador 2"
},
"category": {
"id": 5,
"name": "Mantenimiento"
},
"channel": null,
"proposed_at": null
}