API para integraciones
Gastos
Combustible, recarga y otros gastos, con su ticket.
En esta página
Lista los gastos o sincroniza los cambios
POST/movimientos/gastos/list
- Permiso
lectura
Gastos de la empresa. Hay dos modos, según el cuerpo.
1. Lista con totales (sin updatedSince ni cursor). Gastos activos del filtro, del más
reciente al más antiguo (expenseDate y, a igualdad, expenseId, descendentes), con los totales
de todo el filtro en totals. Como mucho 1000 gastos por respuesta: si el filtro tiene más,
truncated: true y conviene acotarlo (month, vehicleId, types). Este modo no pagina.
2. Sincronización incremental (con updatedSince o cursor). Entrega todos los gastos
cuyo último cambio (alta, corrección, ticket o anulación) cae en (updatedSince, syncedUntil],
anulados incluidos (active: false), en orden de cambio y, a igualdad, por expenseId. Sin
totals ni truncated.
- Primera página:
{ "updatedSince": "<instante>", "limit": 200, ...filtros }. La primera vez,"1970-01-01T00:00:00Z". - Mientras
nextCursorno seanull:{ "cursor": "<nextCursor>", "limit": 200, ...los mismos filtros }. El cursor no guarda los filtros: repítelos en cada página. Si llegancursoryupdatedSince, mandacursor. - Con
nextCursor: nullhas terminado: guardasyncedUntily úsalo comoupdatedSincede la sincronización siguiente (nunca la hora de tu sistema).
syncedUntillo fija la primera página (la hora de la base de datos menos 60 s de margen de asentamiento) y viaja en el cursor: es el mismo en todas las páginas del recorrido. Un cambio tarda hasta 1 minuto en aparecer.- Sin duplicados ni huecos: un gasto que cambia mientras recorres no se repite en ese recorrido; llega en la sincronización siguiente con su versión nueva.
- Si
updatedSinceva por delante de esa hora, la página sale vacía ysyncedUntiles el propioupdatedSince(nunca retrocede). limit(de 1 a 200, por defecto 200) solo vale en este modo.- Los filtros se aplican al estado actual del gasto: uno corregido que deja de cumplirlos (otra fecha, otro vehículo u otro tipo) ya no sale y no se avisa. Para una copia completa, sincroniza sin filtros.
- Lo que borra la purga por retención (ejercicio + 5 años) no se avisa.
Filtros (los dos modos; opcionales y combinables): vehicleId, userId (el conductor del
gasto), tripId (los peajes y aparcamientos ligados a ese viaje), types y el periodo por
expenseDate: month, o from y to (incluidos). month no se combina con from/to.
Con clave API actúas como el owner que la creó: ves los gastos de toda la empresa, de todos los conductores. Leer no exige complementos: los gastos de un complemento que la empresa ya no tiene siguen saliendo.
Errores propios
422 invalid_inputconerror.fields, todos a la vez:month:invalid(mal formado, o junto afrom/to).from,to:invalid(no es una fecha realYYYY-MM-DD);to:out_of_range(from>to).types:invalid(no es una lista, está vacía o trae un tipo desconocido).vehicleId,userId,tripId:invalid(no es un entero positivo).updatedSince:invalid(sin hora, sin zona, fecha imposible o anterior a 1970).cursor:invalid(no es unnextCursoremitido por el servidor).limit:invalid(no es un entero > 0),out_of_range(> 200) ounexpected_field(sinupdatedSincenicursor).- Cualquier otra clave:
unexpected_field.
422 invalid_inputsinfields: el cuerpo no es un objeto JSON.
Petición
Cuerpo obligatorio
Filtros (opcionales y combinables) y, para la sincronización, updatedSince, cursor y limit.
Con updatedSince o cursor la respuesta es una página de la sincronización; sin ellos, la lista
con totales.
vehicleIdinteger | string | nullSolo los gastos de ese vehículo.
Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7userIdinteger | string | nullSolo los gastos de ese conductor.
Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7tripIdinteger | string | nullSolo los peajes y aparcamientos ligados a ese viaje.
Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7typesstring[] | nullSolo esos tipos (lista no vacía; los repetidos se ignoran).
Al menos 1 elementoValores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingmonthstring | nullMes
YYYY-MMdeexpenseDate(del día 1 al último). No se combina confrom/to.Patrón:
^\d{4}-(0[1-9]|1[0-2])$Ejemplo:2026-09fromstring (date) | nullDesde esa
expenseDate(incluida).tostring (date) | nullHasta esa
expenseDate(incluida). No puede ser anterior afrom.updatedSincestring | nullSincronización: entrega lo cambiado después de este instante (exclusivo). ISO 8601 con hora y zona (
Zo±hh:mm); segundos y hasta 6 decimales opcionales; desde 1970. Usa elsyncedUntilde la sincronización anterior.Patrón:
^(\d{4}-\d{2}-\d{2})T([01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,6})?)?(?:Z|[+-](?:0\d|1[0-4]):[0-5]\d)$Ejemplo:2026-10-01T00:00:00Zcursorstring | nullSincronización: el
nextCursorde la página anterior, tal cual (opaco). Si viene, manda sobreupdatedSince.Patrón:
^[A-Za-z0-9_-]{1,200}$limitinteger | nullSincronización: gastos por página (número JSON). Solo con
updatedSinceocursor; si no,unexpected_field.Mínimo
1Máximo200Por defecto:200
Ejemplo: Lista con totales de un mes
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/list" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"month": "2026-09",
"vehicleId": 7,
"types": [
"fuel",
"ev_charge"
]
}'
Otro ejemplo: Primera página de una sincronización
{
"updatedSince": "2026-10-01T00:00:00Z",
"limit": 200
}
Otro ejemplo: Página siguiente de la sincronización
{
"cursor": "eyJ2IjoxLCJ1IjoiMjAyNi0xMC0xM1QwOTo1OTowMC4wMDAwMDBaIiwidCI6IjIwMjYtMTAtMTNUMDg6MTU6MDIuMTIzNDU2WiIsImkiOjgyfQ",
"limit": 200
}
Respuesta
200
Lista con totales o página de la sincronización, según el cuerpo.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataGastosListPage | GastosSyncPageGastosListPagesinupdatedSincenicursor;GastosSyncPagecon alguno de los dos.Una de estas opciones:
GastosListPageLista con totales (gastos activos, del más reciente al más antiguo).
expensesGastosExpense[]Un gasto.
Hasta 1000 elementos
totalsGastosTotalsTotales de todo el filtro, no solo de los gastos devueltos.
truncatedbooleantruesi el filtro tiene más de 1000 gastos y solo se devuelven los 1000 más recientes.
GastosSyncPagePágina de la sincronización incremental (anulados incluidos, en orden de cambio).
expensesGastosExpense[]Como mucho
limitgastos.active: false= anulado.Hasta 200 elementos
nextCursorstring | nullCursor de la página siguiente (con los mismos filtros);
null= no hay más.Patrón:
^[A-Za-z0-9_-]{1,200}$syncedUntilstringInstante (UTC, microsegundos) hasta el que llega este recorrido. Al terminar (
nextCursor: null), es elupdatedSincede la sincronización siguiente.Patrón:
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$Ejemplo:2026-10-13T09:59:00.000000Z
Ejemplo de respuesta: Lista con totales
{
"data": {
"expenses": [
{
"expenseId": 81,
"tenantId": 42,
"vehicleId": 7,
"vehiclePlate": "1234ABC",
"userId": 11,
"userName": "Bea Conductora",
"type": "fuel",
"addon": null,
"expenseDate": "2026-09-10",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"attachment": {
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320
},
"ocrExtractionId": null,
"tripId": null,
"active": true,
"integration": {
"apiKeyId": 7,
"name": "ERP Contable",
"prefix": "fd_live_Ab12"
},
"createdAt": "2026-09-10T08:12:00.000Z",
"updatedAt": "2026-09-10T08:12:00.000Z"
}
],
"totals": {
"count": 1,
"baseAmount": 41.32,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": 0,
"missingBreakdown": 0,
"toReview": 0,
"byType": [
{
"type": "fuel",
"totalAmount": 50
}
]
},
"truncated": false
}
}
Ejemplo de respuesta: Página de la sincronización
{
"data": {
"expenses": [
{
"expenseId": 82,
"tenantId": 42,
"vehicleId": 7,
"vehiclePlate": "1234ABC",
"userId": 11,
"userName": "Bea Conductora",
"type": "toll",
"addon": "tolls_parking",
"expenseDate": "2026-10-12",
"supplierName": "Autopistas AP-6",
"supplierTaxId": null,
"baseAmount": null,
"vatRate": null,
"vatAmount": null,
"totalAmount": 12.4,
"liters": null,
"kwh": null,
"odometerKm": null,
"notes": null,
"attachment": null,
"ocrExtractionId": null,
"tripId": 120,
"active": false,
"integration": null,
"createdAt": "2026-10-12T17:40:11.250Z",
"updatedAt": "2026-10-13T08:15:02.123Z"
}
],
"nextCursor": "eyJ2IjoxLCJ1IjoiMjAyNi0xMC0xM1QwOTo1OTowMC4wMDAwMDBaIiwidCI6IjIwMjYtMTAtMTNUMDg6MTU6MDIuMTIzNDU2WiIsImkiOjgyfQ",
"syncedUntil": "2026-10-13T09:59:00.000000Z"
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 429, 503.
Consulta un gasto
POST/movimientos/gastos/get
- Permiso
lectura
Un gasto activo de la empresa. Un gasto anulado responde 404, igual que uno inexistente o de
otra empresa: los anulados solo salen en la sincronización de list (active: false) y en
history.
Con clave API actúas como el owner que la creó: alcanzas los gastos de todos los conductores. Un
gasto dado de alta por la API lleva en integration la clave con la que se registró.
Errores propios
404 expense_not_found: no existe, está anulado o es de otra empresa (no se distingue).422 invalid_input:fields.expenseId=requiredoinvalid; otra clave,unexpected_field.
Petición
Cuerpo obligatorio
expenseIdinteger | stringobligatorioIdentificador. Entero positivo; también se admite como texto de dígitos (
"81").Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/get" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expenseId": 81
}'
Respuesta
200
El gasto.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataobjectexpenseGastosExpenseUn gasto.
Ejemplo de respuesta
{
"data": {
"expense": {
"expenseId": 81,
"tenantId": 42,
"vehicleId": 7,
"vehiclePlate": "1234ABC",
"userId": 11,
"userName": "Bea Conductora",
"type": "fuel",
"addon": null,
"expenseDate": "2026-10-01",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"attachment": {
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320
},
"ocrExtractionId": null,
"tripId": null,
"active": true,
"integration": {
"apiKeyId": 7,
"name": "ERP Contable",
"prefix": "fd_live_Ab12"
},
"createdAt": "2026-10-01T09:30:12.345Z",
"updatedAt": "2026-10-01T09:30:12.345Z"
}
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 429, 503.
Historial de versiones de un gasto
POST/movimientos/gastos/history
- Permiso
lectura
Versiones selladas de un gasto cuando la empresa tiene activos los registros inalterables:
alta (create), correcciones (correct, también al adjuntar o sustituir el ticket) y anulación
(void), de la primera a la última. Cada versión lleva autor, hora, motivo, la instantánea fiscal
del gasto después del cambio, la huella de su ticket y qué campos cambiaron respecto a la
anterior.
- Vale también para un gasto anulado (su historial termina en
void). - Un gasto de antes de activar el ajuste, o de una empresa sin él, da
versions: []. - Una versión cuyo contenido falta o no se entiende sale con
redacted: truey sin datos: es una alteración, yPOST /movimientos/integridad/verifyda la cadena por rota. - Hasta 500 versiones por gasto.
Con clave API actúas como el owner que la creó: alcanzas los gastos de todos los conductores. El autor de lo registrado por la API es ese owner.
Errores propios
404 expense_not_found: no existe o es de otra empresa.422 invalid_input:fields.expenseId=requiredoinvalid; otra clave,unexpected_field.
Petición
Cuerpo obligatorio
expenseIdinteger | stringobligatorioIdentificador. Entero positivo; también se admite como texto de dígitos (
"81").Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/history" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expenseId": 81
}'
Respuesta
200
Las versiones del gasto (vacía sin registros inalterables).
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataobjectexpenseIdintegerMínimo
1versionsGastosVersion[]De la primera a la última. Vacía si el gasto no tiene versiones selladas.
Hasta 500 elementos
Ejemplo de respuesta
{
"data": {
"expenseId": 81,
"versions": [
{
"seq": 12,
"version": 1,
"change": "create",
"author": {
"userId": 10,
"name": "Ana Titular"
},
"recordedAt": "2026-10-01T09:30:12.345Z",
"reason": null,
"snapshot": {
"vehicleId": 7,
"userId": 11,
"type": "fuel",
"expenseDate": "2026-10-01",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"tripId": null,
"ticketSha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
"files": [
{
"kind": "ticket",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"bytes": 184320
}
],
"changed": [],
"entryHash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"redacted": false
},
{
"seq": 15,
"version": 2,
"change": "correct",
"author": {
"userId": 10,
"name": "Ana Titular"
},
"recordedAt": "2026-10-02T10:02:40.120Z",
"reason": "El importe estaba mal tecleado",
"snapshot": {
"vehicleId": 7,
"userId": 11,
"type": "fuel",
"expenseDate": "2026-10-01",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 45.45,
"vatRate": 21,
"vatAmount": 9.55,
"totalAmount": 55,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"tripId": null,
"ticketSha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
"files": [
{
"kind": "ticket",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"bytes": 184320
}
],
"changed": [
"baseAmount",
"totalAmount",
"vatAmount"
],
"entryHash": "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
"redacted": false
}
]
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 429, 503.
Tipos de gasto disponibles
POST/movimientos/gastos/types-list
- Permiso
lectura
Los nueve tipos de gasto, siempre en el mismo orden, con el complemento que exige cada uno
(addon; null = plan base) y si la empresa lo tiene activo (available).
availableestrueen el plan base o si el complemento está activo (pagado o regalado; en la prueba, solo los regalados). Es la misma regla que aplicasave, que sigue siendo quien decide: guardar un tipo no disponible responde403 feature_not_included.- No dice si la cuenta admite escrituras: eso lo responde cada escritura (
423 account_read_only).
El cuerpo va vacío o como {}.
Errores propios
422 invalid_input: cualquier clave del cuerpo esunexpected_field; sinfieldssi el cuerpo no es un objeto JSON.
Petición
Cuerpo opcional
Sin campos.
object
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/types-list" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Respuesta
200
El catálogo de tipos con su disponibilidad en la empresa.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataobjecttypesGastosTypeAvailability[]Los nueve tipos, en el orden del catálogo.
9 elementos
Ejemplo de respuesta
{
"data": {
"types": [
{
"type": "fuel",
"addon": null,
"available": true
},
{
"type": "ev_charge",
"addon": null,
"available": true
},
{
"type": "toll",
"addon": "tolls_parking",
"available": true
},
{
"type": "parking",
"addon": "tolls_parking",
"available": true
},
{
"type": "maintenance",
"addon": "maintenance_itv",
"available": false
},
{
"type": "itv",
"addon": "maintenance_itv",
"available": false
},
{
"type": "insurance",
"addon": "insurance_tax_renting",
"available": false
},
{
"type": "vehicle_tax",
"addon": "insurance_tax_renting",
"available": false
},
{
"type": "renting",
"addon": "insurance_tax_renting",
"available": false
}
]
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 429, 503.
URL de descarga del ticket
POST/movimientos/gastos/attachment-download-url
- Permiso
lectura
URL prefirmada de S3 para descargar el ticket (foto o PDF) de un gasto activo. Caduca a los
300 segundos (expiresIn).
- Descárgalo con un
GETaurl, sin cabeceraAuthorization: la firma de la URL es la credencial. No la guardes ni la registres en logs. - S3 lo sirve con el
contentTypey elfilenameguardados, comoContent-Disposition: attachment(descarga) o, coninline: true,inline(para mostrarlo). - Es una lectura: funciona aunque la cuenta esté en solo lectura o el mes esté cerrado, y no exige complementos.
- El ticket de un gasto anulado no se puede descargar por aquí (404).
Errores propios
404 expense_not_found: el gasto no existe, está anulado o es de otra empresa.404 attachment_not_found: el gasto no tiene ticket.422 invalid_input:fields.expenseId=requiredoinvalid;fields.inline=invalid(no es booleano); otra clave,unexpected_field.502 storage_unavailable: S3 no pudo firmar la URL. Reintenta.
Petición
Cuerpo obligatorio
expenseIdinteger | stringobligatorioIdentificador. Entero positivo; también se admite como texto de dígitos (
"81").Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81inlineboolean | nulltrue=Content-Disposition: inline(para mostrarlo); por defecto,attachment(descarga).Por defecto:
false
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/attachment-download-url" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expenseId": 81,
"inline": false
}'
Respuesta
200
URL de descarga del ticket.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataobjecturlstring (uri)URL prefirmada de S3 para un
GETsinAuthorization. Es una credencial temporal.filenamestringHasta 120 caracteres
contentTypestringTipo del ticket. Sin SVG ni HTML.
Valores:
application/pdfimage/jpegimage/pngimage/webpsizeBytesintegerMínimo
1Máximo20971520expiresInintegerSegundos de validez de la URL.
Siempre
300
Ejemplo de respuesta
{
"data": {
"url": "https://pfens-flotadoc-documents-production.s3.eu-west-1.amazonaws.com/42/expenses/u10/9f1c2a1b0d4e5f60718293a4b5c6d7e8/ticket-repsol.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-Date=20261013T091500Z&X-Amz-Signature=5d41402abc4b2a76b9719d911017c592",
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320,
"expiresIn": 300
}
}
502
El almacén de documentos no ha respondido (storage_unavailable). Reintenta con espera.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Cuerpo con el formato de los errores.
Ejemplo de respuesta
{
"error": {
"code": "storage_unavailable",
"message": "No se pudo acceder al almacen de documentos"
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 429, 503.
Registra o corrige un gasto
POST/movimientos/gastos/save
- Permiso
registros - Admite
Idempotency-Key
Alta (sin expenseId) o corrección (con expenseId) de un gasto. Es un guardado
completo: manda siempre todos los campos; un campo opcional ausente, null o "" se guarda
vacío (también en una corrección: no mandar tripId suelta el viaje). La excepción es el ticket:
solo cambia si llega attachmentKey con una clave distinta de la que ya tiene.
Con clave API actúas como el owner que la creó (misma empresa, complementos, cuenta, mes cerrado y registros inalterables) y registras en nombre de un conductor:
- En el alta,
driverUserIdes obligatorio (eliddePOST /auth/usuarios/list). El gasto es suyo (userId): sale en su lista y en la del owner. Tiene que poder registrarlo él: persona activa de la empresa con roluseruowner(una invitada que aún no ha entrado vale) y, si esuser, en un vehículo que puede usar (variable, o fijo con él entre sus conductores). - El alta queda marcada con la clave (
integration). Una corrección no cambia ni el conductor ni el origen;driverUserId, si viene, tiene que ser el del gasto. - El autor del sello (registros inalterables) y de la auditoría es el owner de la clave.
ocrExtractionIdes de la app (el OCR no está en la API): la lectura tendría que haberla pedido el propio owner. Una integración manda los campos y lo omite.
Ticket con el gasto. Pide la URL con attachment-upload-url, sube el fichero con el PUT y
manda aquí su key en attachmentKey antes de 24 h: el gasto y su ticket se guardan en la
misma operación (o ninguno). El servidor comprueba en S3 que el fichero está y es una imagen o PDF
de hasta 20 MB, y calcula su huella SHA-256. La subida tiene que haberla pedido el mismo owner
(con cualquiera de sus claves) y no puede estar adjunta a otro gasto. Para adjuntarlo después:
attachment-attach.
Reglas de negocio
fuelsolo en vehículos de combustión o híbridos y conliters;ev_chargesolo en eléctricos o híbridos y conkwh.litersykwhestán prohibidos en los demás tipos.tripIdsolo entollyparking: viaje activo de la empresa y del mismo vehículo. Una corrección que mantiene el mismo viaje y vehículo no lo vuelve a comprobar.- Si llegan
baseAmountyvatAmount, base + cuota = total con ±0,02 € de margen. - Vehículo activo de la empresa; una corrección que no cambia de vehículo admite uno ya de baja.
- Registrar o corregir un tipo de complemento exige tenerlo (en una corrección, también el del tipo
que tenía):
types-listdice cuáles están disponibles. - Mes cerrado: el mes de destino (vehículo y fecha) y, en una corrección, el mes donde está hoy el gasto tienen que estar abiertos.
- Registros inalterables: el alta sella una versión
create(sin motivo); la corrección, unacorrectque exigereason. Sin el ajuste,reasones opcional y no se guarda.
Orden de comprobación: puerta → cuenta (423) → cuerpo (422) → driverUserId obligatorio →
attachmentKey → gasto a corregir (404) y subida del ticket (422) → complemento (403) → vehículo
(404) y energía (422) → conductor (422) → viaje (404/422) → ticket en S3 (422/502) → lectura OCR
(422) → escritura: mes cerrado (409), subida caducada (422), motivo (422).
Errores propios
403 feature_not_included(confeature): el tipo, o en una corrección el que tenía, es de un complemento que la empresa no tiene.404 expense_not_found: corrección de un gasto inexistente, anulado o de otra empresa.404 vehicle_not_found: el vehículo no existe, es de otra empresa o está de baja.404 trip_not_found: el viaje detripIdno existe, está anulado o descartado, o es de otra empresa.409 period_closed(conperiods: [{ vehicleId, month: "YYYY-MM" }]): algún mes afectado está cerrado; hay que pedir a la empresa que lo reabra. También409 idempotency_*de la puerta.422 invalid_inputconerror.fields. Los de formato llegan todos a la vez:required,invalid,too_long,out_of_range,in_future(expenseDateposterior a hoy en Madrid),mismatch(totalAmountno cuadra con base + cuota),not_for_type(liters,kwhotripIden un tipo que no los admite),unexpected_field. Llegan solos, porque se comprueban después:driverUserId=required(alta sin conductor),not_found(no es conductor activo de la empresa),vehicle_not_allowed(no puede usar ese vehículo) oimmutable(corrección con otro conductor);attachmentKey=invalid(no es una clave emitida para este owner en esta empresa);type=not_for_vehicle(energía del vehículo);tripId=not_for_vehicle(el viaje es de otro vehículo);ocrExtractionId=invalid.422 reason_required: corrección sinreasoncon registros inalterables (no se guardó nada).422 attachment_not_uploaded: la subida deattachmentKeyno está registrada, tiene más de 24 h, se descartó, ya es el ticket de otro gasto, o el fichero no está en S3 (no se hizo elPUTo no terminó). Vuelve a subirlo.422 attachment_invalid: el fichero de S3 no es una imagen JPEG/PNG/WebP o un PDF de 1 byte a 20 MB.423 account_read_only(conreason:account,suspendedopending_payment): la cuenta no admite registros (prueba, contrato o pago vencidos). Leer sigue funcionando.502 storage_unavailable: S3 no respondió al comprobar o leer el ticket.
Petición
Cabeceras
Idempotency-KeystringClave de idempotencia de la escritura (opcional, recomendada). Vale 24 horas por clave API: la misma
Idempotency-Keycon la misma ruta y el mismo cuerpo exacto devuelve la respuesta guardada conIdempotent-Replayed: truesin volver a ejecutar la operación. Con otro cuerpo u otra ruta:409 idempotency_conflict; si la primera aún está en curso:409 idempotency_in_progress. Una respuesta 5xx no se guarda (se puede reintentar). Mal formada:422 invalid_idempotency_key.Patrón:
^[A-Za-z0-9_-]{1,100}$
Cuerpo obligatorio
Guardado completo: un campo opcional ausente, null o "" se guarda vacío. Los importes y
cantidades admiten número JSON o texto con punto decimal ("41.32").
expenseIdinteger | string | nullAusente o
null= alta. Un id = corrección de ese gasto (activo).Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7driverUserIdinteger | string | nullConductor del gasto. Con clave API, obligatorio en el alta: persona activa de la empresa con rol
useruownerque pueda usar el vehículo. En una corrección se puede omitir; si viene, tiene que ser el conductor del gasto.Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7vehicleIdinteger | stringobligatorioVehículo activo de la empresa (en una corrección que no lo cambia, también uno de baja).
Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81typestringobligatorioTipo de gasto.
fuel(combustible) yev_charge(recarga eléctrica) son del plan base;toll(peaje) yparking(aparcamiento) exigen el complementotolls_parking;maintenance(mantenimiento o taller) eitv,maintenance_itv;insurance(seguro),vehicle_tax(impuesto de circulación) yrenting(cuota de renting),insurance_tax_renting.Valores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingexpenseDatestring (date)obligatorioFecha del ticket. Real, desde
2000-01-01y no posterior a hoy en Europe/Madrid.totalAmountnumber | stringobligatorioTotal en euros, > 0, hasta 2 decimales. Si vienen base y cuota, base + cuota = total ± 0,02.
Mayor que
0Máximo999999.99Patrón:^\d{1,9}(\.\d{1,2})?$Ejemplo:50baseAmountnumber | string | nullBase imponible en euros, ≥ 0, hasta 2 decimales.
Mínimo
0Máximo999999.99Patrón:^(\d{1,9}(\.\d{1,2})?)?$vatRatenumber | string | nullTipo de IVA en %, de 0 a 100, hasta 2 decimales.
Mínimo
0Máximo100Patrón:^(\d{1,9}(\.\d{1,2})?)?$vatAmountnumber | string | nullCuota de IVA en euros, ≥ 0, hasta 2 decimales.
Mínimo
0Máximo999999.99Patrón:^(\d{1,9}(\.\d{1,2})?)?$supplierNamestring | nullProveedor. Se recortan los espacios y se colapsan los internos; el límite cuenta después.
Hasta 200 caracteres
supplierTaxIdstring | nullNIF del proveedor. Se quitan espacios, puntos y guiones y pasa a mayúsculas; después tiene que quedar de 2 a 20 caracteres
A-Z0-9("B-12.345.678"→B12345678). No se comprueba la letra de control (hay proveedores de fuera).litersnumber | string | nullLitros, > 0, hasta 3 decimales. Obligatorio en
fuely prohibido en el resto (not_for_type).Mayor que
0Máximo9999.999Patrón:^(\d{1,9}(\.\d{1,3})?)?$kwhnumber | string | nullkWh, > 0, hasta 3 decimales. Obligatorio en
ev_chargey prohibido en el resto (not_for_type).Mayor que
0Máximo9999.999Patrón:^(\d{1,9}(\.\d{1,3})?)?$odometerKminteger | string | nullCuentakilómetros, entero de 0 a 9 999 999 (número JSON;
""onull= sin dato).Mínimo
0Máximo9999999Hasta 0 caracteresnotesstring | nullNotas. Se recortan los espacios de los extremos; el límite cuenta después.
Hasta 500 caracteres
tripIdinteger | string | nullViaje en el que se pagó. Solo en
tollyparking; viaje activo del mismo vehículo.Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7ocrExtractionIdinteger | string | nullLectura OCR con la que se rellenó (uso de la app; el OCR no está en la API). Tiene que ser una lectura válida pedida por el owner de la clave; si no,
invalid. En una corrección, sin él se conserva la que tuviera.Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:7attachmentKeystring | nullLa
keydeattachment-upload-url, tal cual: el ticket se guarda con el gasto. Ausente onull= el ticket no cambia (igual que la clave del ticket que ya tiene).De 1 a 512 caracteresPatrón:
^[1-9]\d*/expenses/u[1-9]\d*/[0-9a-f]{32}/[A-Za-z0-9][A-Za-z0-9._-]{0,119}$reasonstring | nullMotivo de la corrección (se recortan los espacios; vacío = sin motivo). Obligatorio en una corrección con registros inalterables. En un alta no se usa.
Hasta 500 caracteres
Ejemplo: Alta de un repostaje con su ticket, en nombre de un conductor
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/save" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"driverUserId": 11,
"vehicleId": 7,
"type": "fuel",
"expenseDate": "2026-10-01",
"totalAmount": 50,
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"liters": 30.12,
"odometerKm": 120800,
"notes": null,
"attachmentKey": "42/expenses/u10/9f1c2a1b0d4e5f60718293a4b5c6d7e8/ticket-repsol.jpg"
}'
Otro ejemplo: Corrección con motivo (registros inalterables)
{
"expenseId": 81,
"vehicleId": 7,
"type": "fuel",
"expenseDate": "2026-10-01",
"totalAmount": 55,
"baseAmount": 45.45,
"vatRate": 21,
"vatAmount": 9.55,
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"liters": 30.12,
"odometerKm": 120800,
"reason": "El importe estaba mal tecleado"
}
Respuesta
200
El gasto guardado.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Idempotent-Replayedstringtruesi es la respuesta guardada de una petición anterior con la mismaIdempotency-Key.Valores:
true
dataobjectexpenseGastosExpenseUn gasto.
Ejemplo de respuesta
{
"data": {
"expense": {
"expenseId": 81,
"tenantId": 42,
"vehicleId": 7,
"vehiclePlate": "1234ABC",
"userId": 11,
"userName": "Bea Conductora",
"type": "fuel",
"addon": null,
"expenseDate": "2026-10-01",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"attachment": {
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320
},
"ocrExtractionId": null,
"tripId": null,
"active": true,
"integration": {
"apiKeyId": 7,
"name": "ERP Contable",
"prefix": "fd_live_Ab12"
},
"createdAt": "2026-10-01T09:30:12.345Z",
"updatedAt": "2026-10-01T09:30:12.345Z"
}
}
}
502
El almacén de documentos no ha respondido al comprobar el ticket (storage_unavailable). No se guardó nada.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Cuerpo con el formato de los errores.
Ejemplo de respuesta
{
"error": {
"code": "storage_unavailable",
"message": "No se pudo acceder al almacen de documentos"
}
}
Además, las respuestas comunes: 401, 403, 404, 409, 422, 423, 429, 503.
Anula un gasto
POST/movimientos/gastos/delete
- Permiso
registros - Admite
Idempotency-Key
Anula un gasto activo. Es un borrado lógico: el gasto y su ticket se conservan como justificante
fiscal, pero sale de las listas, de los totales y del precio del combustible del mes. La
sincronización de list lo entrega después con active: false.
- No exige complemento: se puede anular un peaje aunque la empresa ya no tenga ese complemento.
- Registros inalterables: anular sella una versión
voidy exigereason. Sin el ajuste,reasones opcional y no se guarda. - Mes cerrado: si el mes del gasto (su vehículo y su fecha) está cerrado, no se anula.
Con clave API actúas como el owner que la creó: puedes anular cualquier gasto de la empresa.
Errores propios
404 expense_not_found: no existe, ya está anulado o es de otra empresa.409 period_closed(conperiods: [{ vehicleId, month: "YYYY-MM" }]): el mes del gasto está cerrado. También409 idempotency_*de la puerta.422 invalid_input:fields.expenseId=requiredoinvalid;fields.reason=invalidotoo_long; otra clave,unexpected_field.422 reason_required: sinreasoncon registros inalterables (no se anuló).423 account_read_only(conreason:account,suspendedopending_payment).
Petición
Cabeceras
Idempotency-KeystringClave de idempotencia de la escritura (opcional, recomendada). Vale 24 horas por clave API: la misma
Idempotency-Keycon la misma ruta y el mismo cuerpo exacto devuelve la respuesta guardada conIdempotent-Replayed: truesin volver a ejecutar la operación. Con otro cuerpo u otra ruta:409 idempotency_conflict; si la primera aún está en curso:409 idempotency_in_progress. Una respuesta 5xx no se guarda (se puede reintentar). Mal formada:422 invalid_idempotency_key.Patrón:
^[A-Za-z0-9_-]{1,100}$
Cuerpo obligatorio
expenseIdinteger | stringobligatorioIdentificador. Entero positivo; también se admite como texto de dígitos (
"81").Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81reasonstring | nullMotivo de la anulación (se recortan los espacios). Obligatorio con registros inalterables.
Hasta 500 caracteres
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/delete" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"expenseId": 81,
"reason": "Ticket duplicado"
}'
Respuesta
200
El gasto ha quedado anulado.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Idempotent-Replayedstringtruesi es la respuesta guardada de una petición anterior con la mismaIdempotency-Key.Valores:
true
dataobjectdeletedbooleanSiempre
trueexpenseIdintegerMínimo
1
Ejemplo de respuesta
{
"data": {
"deleted": true,
"expenseId": 81
}
}
Además, las respuestas comunes: 401, 403, 404, 409, 422, 423, 429, 503.
URL de subida del ticket
POST/movimientos/gastos/attachment-upload-url
- Permiso
registros
Primer paso para guardar el ticket (foto o PDF) de un gasto. Declaras nombre, tipo y tamaño
exacto del fichero y el servidor compone la clave de S3 y firma un PUT directo a S3: los bytes
nunca pasan por la API. No pide gasto: el ticket se sube antes de guardarlo.
Subida (PUT). En menos de 300 s (expiresIn):
PUT <url>
Content-Type: <contentType, el mismo que declaraste>
Content-Length: <sizeBytes, exactamente>
<bytes del fichero>
Content-TypeyContent-Lengthvan en la firma (X-Amz-SignedHeaders=content-length;content-type;host): otro tipo u otro tamaño hace que S3 responda403y no guarde nada.- Sin cabecera
Authorization: la firma de la URL es la credencial. No la guardes ni la registres en logs. S3 responde200con el cuerpo vacío. - Por ejemplo:
curl -X PUT "$URL" -H 'Content-Type: image/jpeg' --data-binary @ticket.jpg.
Después, antes de 24 h desde esta llamada, liga el ticket a un gasto con su key:
save con attachmentKey (alta o corrección; el gasto y el ticket se guardan juntos) o
attachment-attach (gasto ya existente). Una subida que nadie adjunta en 24 h se borra sola.
- Cada llamada crea una subida nueva (
keydistinta, aunque el nombre se repita): nunca pisa un ticket ya adjuntado. filenamevuelve saneado: es el nombre con el que se guardará y descargará el ticket.- Con clave API, la subida es del owner que creó la clave (la
keyllevau{su id}), aunque el gasto sea de un conductor; solo ese owner (con cualquiera de sus claves) puede adjuntarla. - Sin
Idempotency-Key: la operación solo firma una URL, así que no guarda respuesta y la cabecera se ignora (como en las lecturas). Repetir la llamada da otraurlválida y otrakey, nunca una URL caducada; la subida que no uses se borra sola a las 24 h. Si se corta la conexión, vuelve a pedirla.
Errores propios
413 payload_too_large(conmaxBytes: 20971520):sizeBytessupera 20 MB y el resto del cuerpo es válido.422 invalid_inputconerror.fields:filename=requiredoinvalid(no queda nada útil al sanearlo);contentType=requiredoinvalid(no esapplication/pdf,image/jpeg,image/pngniimage/webp);sizeBytes=required,invalid(no es un entero > 0) otoo_large(junto a otros errores); otra clave,unexpected_field.423 account_read_only(conreason:account,suspendedopending_payment): se comprueba antes que el cuerpo.502 storage_unavailable: S3 no pudo firmar. Pide otra URL.
Petición
Cuerpo obligatorio
filenamestringobligatorioNombre del fichero. Se sanea: último segmento de la ruta, solo
A-Z a-z 0-9 . _ -(lo demás pasa a_), sin.., empezando por letra o dígito y recortado a 120 caracteres conservando la extensión.Al menos 1 carácterEjemplo:
ticket-repsol.jpgcontentTypestringobligatorioTipo del ticket. Sin SVG ni HTML.
Valores:
application/pdfimage/jpegimage/pngimage/webpsizeBytesintegerobligatorioTamaño exacto del fichero en bytes (hasta 20 MB). Va en la firma del
PUT.Mínimo
1Máximo20971520
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/attachment-upload-url" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320
}'
Respuesta
200
URL prefirmada para el PUT y la key con la que ligar el ticket al gasto.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
dataobjecturlstring (uri)URL prefirmada de S3 para el
PUT(sinAuthorization). Es una credencial temporal.keystring{empresa}/expenses/u{quien sube}/{id de subida}/{fichero}. Mándala ensave(attachmentKey) o enattachment-attach(key) antes de 24 h.Hasta 512 caracteresPatrón:
^[1-9]\d*/expenses/u[1-9]\d*/[0-9a-f]{32}/[A-Za-z0-9][A-Za-z0-9._-]{0,119}$filenamestringEl nombre ya saneado.
Hasta 120 caracteres
contentTypestringTipo del ticket. Sin SVG ni HTML.
Valores:
application/pdfimage/jpegimage/pngimage/webpsizeBytesintegerMínimo
1Máximo20971520expiresInintegerSegundos de validez de la URL.
Siempre
300
Ejemplo de respuesta
{
"data": {
"url": "https://pfens-flotadoc-documents-production.s3.eu-west-1.amazonaws.com/42/expenses/u10/9f1c2a1b0d4e5f60718293a4b5c6d7e8/ticket-repsol.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost&X-Amz-Date=20261013T091500Z&X-Amz-Signature=5d41402abc4b2a76b9719d911017c592",
"key": "42/expenses/u10/9f1c2a1b0d4e5f60718293a4b5c6d7e8/ticket-repsol.jpg",
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320,
"expiresIn": 300
}
}
413
El fichero declarado supera el tamaño máximo (payload_too_large, con maxBytes).
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Cuerpo con el formato de los errores.
Ejemplo de respuesta
{
"error": {
"code": "payload_too_large",
"message": "El ticket supera el tamano maximo",
"maxBytes": 20971520
}
}
502
S3 no pudo firmar la URL (storage_unavailable). Pide otra.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Cuerpo con el formato de los errores.
Ejemplo de respuesta
{
"error": {
"code": "storage_unavailable",
"message": "No se pudo acceder al almacen de documentos"
}
}
Además, las respuestas comunes: 401, 403, 404, 422, 423, 429, 503.
Adjunta o sustituye el ticket de un gasto
POST/movimientos/gastos/attachment-attach
- Permiso
registros - Admite
Idempotency-Key
Liga a un gasto ya existente el ticket subido con attachment-upload-url y el PUT. Si el
gasto ya tenía ticket, lo sustituye. Para un alta con ticket es más simple mandar la key en
save (attachmentKey).
- La
keytiene que ser una subida del mismo owner (con cualquiera de sus claves), de menos de 24 h y no adjunta a otro gasto. El servidor comprueba en S3 que el fichero está y es una imagen o PDF de hasta 20 MB, y calcula su huella SHA-256. - Repetir la llamada con la
keyque ya es el ticket del gasto responde200sin cambiar nada. - Adjuntar a un gasto de complemento exige tener el complemento.
- Mes cerrado: el ticket de un mes cerrado queda congelado (tampoco se sustituye).
- Registros inalterables: adjuntar es una corrección sellada (
correct). El primer ticket de un gasto que no tenía no necesitareason(el servidor pone «Se añade el ticket»); sustituirlo exigereason. El ticket sustituido se conserva en la versión anterior. Sin el ajuste,reasones opcional y no se guarda, y el ticket sustituido se descarta.
Con clave API actúas como el owner que la creó: puedes adjuntar el ticket a un gasto de cualquier conductor de la empresa.
Errores propios
403 feature_not_included(confeature): el gasto es de un complemento que la empresa no tiene.404 expense_not_found: el gasto no existe, está anulado o es de otra empresa.409 period_closed(conperiods: [{ vehicleId, month: "YYYY-MM" }]): el mes del gasto está cerrado. También409 idempotency_*de la puerta.422 invalid_inputconerror.fields:expenseId=requiredoinvalid;key=requiredoinvalid(también, sola, si no es una clave emitida para este owner en esta empresa);reason=invalidotoo_long; otra clave,unexpected_field.422 attachment_not_uploaded: la subida no está registrada, tiene más de 24 h, se descartó, ya es el ticket de otro gasto, o el fichero no está en S3. Vuelve a subirlo.422 attachment_invalid: el fichero de S3 no es una imagen JPEG/PNG/WebP o un PDF de 1 byte a 20 MB.422 reason_required: sustituir el ticket sinreasoncon registros inalterables (no se cambió nada).423 account_read_only(conreason:account,suspendedopending_payment).502 storage_unavailable: S3 no respondió al comprobar o leer el fichero.
Petición
Cabeceras
Idempotency-KeystringClave de idempotencia de la escritura (opcional, recomendada). Vale 24 horas por clave API: la misma
Idempotency-Keycon la misma ruta y el mismo cuerpo exacto devuelve la respuesta guardada conIdempotent-Replayed: truesin volver a ejecutar la operación. Con otro cuerpo u otra ruta:409 idempotency_conflict; si la primera aún está en curso:409 idempotency_in_progress. Una respuesta 5xx no se guarda (se puede reintentar). Mal formada:422 invalid_idempotency_key.Patrón:
^[A-Za-z0-9_-]{1,100}$
Cuerpo obligatorio
expenseIdinteger | stringobligatorioIdentificador. Entero positivo; también se admite como texto de dígitos (
"81").Mínimo
1Máximo2147483647Patrón:^[1-9]\d{0,15}$Ejemplo:81keystringobligatorioLa
keydeattachment-upload-url, tal cual.De 1 a 512 caracteresPatrón:
^[1-9]\d*/expenses/u[1-9]\d*/[0-9a-f]{32}/[A-Za-z0-9][A-Za-z0-9._-]{0,119}$reasonstring | nullMotivo (se recortan los espacios). Con registros inalterables, obligatorio para sustituir un ticket; para el primero, opcional (el servidor pone «Se añade el ticket»).
Hasta 500 caracteres
Ejemplo
curl -sS -X POST "https://app.flotadoc.com/api/movimientos/gastos/attachment-attach" \
-H "Authorization: Bearer $FLOTADOC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"expenseId": 81,
"key": "42/expenses/u10/9f1c2a1b0d4e5f60718293a4b5c6d7e8/ticket-repsol.jpg"
}'
Respuesta
200
El gasto con su ticket.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Idempotent-Replayedstringtruesi es la respuesta guardada de una petición anterior con la mismaIdempotency-Key.Valores:
true
dataobjectexpenseGastosExpenseUn gasto.
Ejemplo de respuesta
{
"data": {
"expense": {
"expenseId": 81,
"tenantId": 42,
"vehicleId": 7,
"vehiclePlate": "1234ABC",
"userId": 11,
"userName": "Bea Conductora",
"type": "fuel",
"addon": null,
"expenseDate": "2026-10-01",
"supplierName": "Repsol Getafe",
"supplierTaxId": "A12345678",
"baseAmount": 41.32,
"vatRate": 21,
"vatAmount": 8.68,
"totalAmount": 50,
"liters": 30.12,
"kwh": null,
"odometerKm": 120800,
"notes": null,
"attachment": {
"filename": "ticket-repsol.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320
},
"ocrExtractionId": null,
"tripId": null,
"active": true,
"integration": {
"apiKeyId": 7,
"name": "ERP Contable",
"prefix": "fd_live_Ab12"
},
"createdAt": "2026-10-01T09:30:12.345Z",
"updatedAt": "2026-10-01T11:02:05.871Z"
}
}
}
502
El almacén de documentos no ha respondido (storage_unavailable). No se adjuntó nada.
Cabeceras
X-Request-Idstring (uuid)Id de la petición en el historial de la empresa (para soporte).
Cuerpo con el formato de los errores.
Ejemplo de respuesta
{
"error": {
"code": "storage_unavailable",
"message": "No se pudo acceder al almacen de documentos"
}
}
Además, las respuestas comunes: 401, 403, 404, 409, 422, 423, 429, 503.
Modelos
Objetos que devuelven o reciben las operaciones de esta área. Cada campo enlaza a su modelo.
GastosExpenseobject
Un gasto.
expenseIdintegerMínimo
1tenantIdintegerEmpresa del gasto.
Mínimo
1vehicleIdintegerMínimo
1vehiclePlatestring | nullMatrícula del vehículo (
nullsi no se pudo leer).userIdintegerConductor del gasto (de quién es). Por la API, el
driverUserIddel alta.Mínimo
1userNamestring | nullNombre del conductor (
nullsi no se pudo leer).typestringTipo de gasto.
fuel(combustible) yev_charge(recarga eléctrica) son del plan base;toll(peaje) yparking(aparcamiento) exigen el complementotolls_parking;maintenance(mantenimiento o taller) eitv,maintenance_itv;insurance(seguro),vehicle_tax(impuesto de circulación) yrenting(cuota de renting),insurance_tax_renting.Valores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingaddonstring | nullComplemento que exige el tipo;
null= plan base.Valores:
tolls_parkingmaintenance_itvinsurance_tax_rentingexpenseDatestring (date)Fecha del ticket (día fiscal Europe/Madrid).
supplierNamestring | nullHasta 200 caracteres
supplierTaxIdstring | nullNIF del proveedor, normalizado (sin espacios, puntos ni guiones, en mayúsculas).
Patrón:
^[A-Z0-9]{2,20}$baseAmountnumber | nullBase imponible en euros.
Mínimo
0Máximo999999.99vatRatenumber | nullTipo de IVA en %.
Mínimo
0Máximo100vatAmountnumber | nullCuota de IVA en euros.
Mínimo
0Máximo999999.99totalAmountnumberTotal en euros.
Mayor que
0Máximo999999.99litersnumber | nullLitros (solo en
fuel, donde siempre vienen).Mayor que
0Máximo9999.999kwhnumber | nullkWh (solo en
ev_charge, donde siempre vienen).Mayor que
0Máximo9999.999odometerKminteger | nullLectura del cuentakilómetros.
Mínimo
0Máximo9999999notesstring | nullHasta 500 caracteres
attachmentGastosAttachment | nullEl ticket, o
nullsi no tiene.ocrExtractionIdinteger | nullLectura OCR con la que se rellenó en la app;
nullsi no.Mínimo
1tripIdinteger | nullViaje del peaje o aparcamiento;
nullsi no está ligado a un viaje.Mínimo
1activebooleanfalsesi está anulado (solo en la sincronización delist).integrationGastosIntegration | nullClave API con la que se dio de alta;
nullsi lo registró una persona en la web o la app.createdAtstring (date-time)Alta (UTC, con milisegundos).
updatedAtstring (date-time)Último cambio (UTC, con milisegundos).
GastosTotalsobject
Totales de todo el filtro, no solo de los gastos devueltos.
countintegerGastos del filtro.
Mínimo
0baseAmountnumberSuma de las bases (solo de los gastos que la tienen).
Mínimo
0vatAmountnumberSuma de las cuotas de IVA (solo de los gastos que la tienen).
Mínimo
0totalAmountnumberMínimo
0litersnumberMínimo
0kwhnumberMínimo
0missingBreakdownintegerGastos sin base o sin cuota.
Mínimo
0toReviewintegerGastos por revisar (sin ticket, sin base o sin cuota).
Mínimo
0byTypeGastosTypeTotal[]Total de cada tipo con algún gasto en el filtro, en el orden del catálogo de tipos.
GastosVersionobject
seqintegerNúmero de la entrada en la cadena de la empresa.
Mínimo
1versionintegerNúmero de versión dentro del gasto (1, 2, 3…).
Mínimo
1changestringcreatealta,correctcorrección (o ticket),voidanulación,technicalversión técnica.Valores:
createcorrectvoidtechnicalauthorGastosVersionAuthor | nullQuién hizo el cambio;
nullsolo en una versión técnica.recordedAtstring (date-time)Hora del servidor (UTC).
reasonstring | nullMotivo de la corrección o anulación.
Hasta 500 caracteres
snapshotGastosSnapshot | nullInstantánea fiscal del gasto después del cambio;
nullsi la versión estáredacted.filesGastosVersionFile[]Huellas de los ficheros de la versión (el ticket vigente). Vacía sin ticket.
changedstring[]Claves de
snapshotque cambiaron respecto a la versión anterior (orden alfabético;[]en la primera).ticketSha256= se sustituyó el ticket.entryHashstringHuella SHA-256 de la entrada en la cadena.
Patrón:
^[0-9a-f]{64}$redactedbooleantruesi el contenido falta o no se entiende (alteración: la verificación da la cadena por rota).
GastosTypeAvailabilityobject
typestringTipo de gasto.
fuel(combustible) yev_charge(recarga eléctrica) son del plan base;toll(peaje) yparking(aparcamiento) exigen el complementotolls_parking;maintenance(mantenimiento o taller) eitv,maintenance_itv;insurance(seguro),vehicle_tax(impuesto de circulación) yrenting(cuota de renting),insurance_tax_renting.Valores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingaddonstring | nullComplemento que exige;
null= plan base.Valores:
tolls_parkingmaintenance_itvinsurance_tax_rentingavailablebooleantruesi la empresa puede registrar este tipo (plan base o complemento activo).
GastosAttachmentobject
Ticket adjunto. La clave de S3 nunca sale en las lecturas.
filenamestringNombre saneado del fichero (
A-Z a-z 0-9 . _ -).Hasta 120 caracteresEjemplo:
ticket-repsol.jpgcontentTypestringTipo del ticket. Sin SVG ni HTML.
Valores:
application/pdfimage/jpegimage/pngimage/webpsizeBytesintegerTamaño en bytes del fichero guardado.
Mínimo
1Máximo20971520
GastosIntegrationobject
Clave API con la que se dio de alta el gasto.
apiKeyIdintegerId de la clave (también si ya está revocada).
Mínimo
1namestring | nullNombre de la clave.
nullsi no se pudo leer.prefixstring | nullPrefijo visible de la clave (
fd_live_+ 4 caracteres).nullsi no se pudo leer.Ejemplo:
fd_live_Ab12
GastosTypeTotalobject
typestringTipo de gasto.
fuel(combustible) yev_charge(recarga eléctrica) son del plan base;toll(peaje) yparking(aparcamiento) exigen el complementotolls_parking;maintenance(mantenimiento o taller) eitv,maintenance_itv;insurance(seguro),vehicle_tax(impuesto de circulación) yrenting(cuota de renting),insurance_tax_renting.Valores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingtotalAmountnumber
GastosVersionAuthorobject
userIdintegerMínimo
1namestring | nullnullsi es alguien que no se ve desde la empresa (equipo de FlotaDoc).
GastosSnapshotobject
Instantánea fiscal sellada del gasto. Los importes van como número JSON.
vehicleIdintegeropcionaluserIdintegeropcionalConductor del gasto.
typestringopcionalTipo de gasto.
fuel(combustible) yev_charge(recarga eléctrica) son del plan base;toll(peaje) yparking(aparcamiento) exigen el complementotolls_parking;maintenance(mantenimiento o taller) eitv,maintenance_itv;insurance(seguro),vehicle_tax(impuesto de circulación) yrenting(cuota de renting),insurance_tax_renting.Valores:
fuelev_chargetollparkingmaintenanceitvinsurancevehicle_taxrentingexpenseDatestring (date)opcionalsupplierNamestring | nullopcionalsupplierTaxIdstring | nullopcionalbaseAmountnumber | nullopcionalvatRatenumber | nullopcionalvatAmountnumber | nullopcionaltotalAmountnumberopcionallitersnumber | nullopcionalkwhnumber | nullopcionalodometerKminteger | nullopcionalnotesstring | nullopcionaltripIdinteger | nullopcionalticketSha256string | nullopcionalSHA-256 del ticket vigente;
nullsin ticket.Patrón:
^[0-9a-f]{64}$
GastosVersionFileobject
kindstringTipo de fichero. En un gasto, siempre
ticket.Valores:
ticketsignaturetrackreportsha256stringPatrón:
^[0-9a-f]{64}$bytesintegeropcionalTamaño en bytes (si se conoce).
Mínimo
0
¿Quieres conectar tu sistema?
Cuéntanos qué quieres integrar y lo vemos en una llamada: qué datos necesitas, con qué permisos y cada cuánto.
Si tu empresa ya usa FlotaDoc, el titular puede pedir el acceso desde la web, en Mi cuenta › Integraciones.