Documentación de la API de DecaDriver
Todo lo que hace falta para que tu ERP, CRM o TMS emita el DeCA con una petición HTTP y sepa después qué ha cambiado, incluido lo que registra el conductor en ruta. Sin SDK: cualquier sistema que haga un POST con JSON sirve.
DecaDriver está en desarrollo de cara a la entrada en vigor del 5 de octubre de 2026. Esta página describe la API tal como está construida hoy; lo que llega después está en Estado del producto. Las claves se crean desde el panel de cada empresa cliente.
La clave y GET /yo
Cada empresa tiene dos claves: una de prueba y una real. Van en la cabecera Authorization y en ningún otro sitio.
- Base:
https://decadriver.es/api/v1. Todo es JSON en UTF-8: un cuerpo en otra codificación (Windows-1252, Latin-1) se rechaza con 400 peticion.json_invalido, para que las eñes y los acentos no lleguen rotos al DeCA. - dd_test_… crea y ve solo servicios de prueba; dd_live_…, solo los reales. Se crean en el panel, en Ajustes → API, y se enseñan una sola vez: guárdala en el gestor de secretos de tu servidor.
- Ninguna URL lleva la clave. Si la API la encuentra en una URL, rechaza la petición (acaba en registros e historiales): sustitúyela.
- Llama desde tu servidor, nunca desde un navegador ni una app móvil: la API no admite peticiones de otros orígenes (CORS cerrado) y la clave quedaría a la vista.
- Cada respuesta, también las de error, lleva la cabecera
X-Request-Id. Cítala si nos escribes.
Antes de tocar datos, comprueba que la clave y la red funcionan:
curl -s https://decadriver.es/api/v1/yo \
-H "Authorization: Bearer $DECADRIVER_CLAVE"{
"ok": true,
"empresa": { "id": "k3Vq9mTz0bXa7LcE", "razon_social": "Transportes Ejemplo SL", "nif": "B87654323", "zona_horaria": "Europe/Madrid" },
"entorno": "test",
"es_prueba": true,
"clave": { "prefijo": "dd_test_Ab3dE9xQ…", "creada_en": "2026-10-01T08:12:40.118Z" },
"version_api": "v1",
"servidor_ahora": "2026-10-01T08:15:02.531Z",
"request_id": "req_Qm2x8VtLz0aB4nKe"
}Tu primer DeCA en cinco minutos
Un POST con los datos del servicio devuelve el DeCA emitido: número, URL pública y QR, en la misma respuesta.
- Crea la clave de prueba en Ajustes → API y ponla en la variable de entorno DECADRIVER_CLAVE.
- Llama a GET /yo: tiene que responder con tu empresa y «entorno»: «test».
- Manda el servicio con POST /servicios (cuerpo de abajo, datos ficticios).
- Abre url_publica: es el PDF, con la marca «PRUEBA – SIN VALIDEZ». Es la URL que va en el QR.
- Repite exactamente la misma petición: responde 200 con X-Idempotent-Replay: true y el mismo servicio. No se ha creado otro.
{
"referencia": "PED-0417",
"cargador_razon": "Conservas Ejemplo SL",
"cargador_nif": "B12345674",
"cargador_domicilio": "Polígono Norte, nave 4, Lugo",
"transportista_razon": "Transportes Ejemplo SL",
"transportista_nif": "B87654323",
"origen_texto": "Lugo",
"destino_texto": "Vigo, puerto",
"mercancia_naturaleza": "Conservas en palés",
"peso_kg": 12500,
"cantidad": "32 palés",
"matricula_tractora": "1234 BCD",
"matricula_remolque": "R 5678 FGH",
"fecha_transporte": "2026-10-06",
"hora_prevista": "07:00",
"observaciones": "Muelle 4",
"conductor": { "telefono": "+34600000000" }
}HTTP/1.1 201 Created
X-Request-Id: req_Tn7c1RwXq5sG0hJd
Location: https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa
{
"ok": true,
"id": "Yq3m0bZr8TfQ2nXa",
"referencia": "PED-0417",
"numero": "PRUEBA-000001",
"estado": "emitido",
"es_prueba": true,
"version": 1,
"url_publica": "https://decadriver.es/d/prueba/3fK9qLm2VxT0bZr8aYc1Ne",
"qr_svg": "https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/qr.svg",
"qr_png": "https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/qr.png",
"pdf": "https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/pdf",
"conductor": null,
"fechas": { "creado_en": "2026-10-01T08:16:10.204Z", "emitido_en": "2026-10-01T08:16:10.911Z", "…": "…" },
"datos": { "cargador_razon": "Conservas Ejemplo SL", "peso_kg": 12500, "matricula_tractora": "1234BCD", "…": "…" },
"avisos": [
{ "campo": "conductor", "codigo": "conductor.no_encontrado", "mensaje": "Ese dato no es de ningún conductor activo de la empresa: el envío queda sin asignar." }
],
"request_id": "req_Tn7c1RwXq5sG0hJd"
}Lo que hay que saber de esta llamada:
- Crea y emite. Con
"emitir": falsela deja en la bandeja del panel como «listo» (o «asignado» si casó el conductor) para emitirla después. - Idempotente por referencia. Misma referencia y mismos datos: 200 y X-Idempotent-Replay: true. Datos distintos sobre un envío aún no emitido: se actualiza (200, actualizado: true). Sobre uno emitido: 409 con diferencias[]; un documento emitido solo cambia con una modificación con motivo.
- Sin referencia no hay idempotencia. Se asigna una del tipo «DD-{id}» y llega el aviso referencia.generada: si repites la petición tras un corte, se crea otro servicio. Manda siempre tu número de pedido o expediente.
- La fila se guarda antes de generar el PDF. Si se corta la conexión o responde 503, reintenta la misma petición: recibirás el servicio ya creado, y emitido si faltaba.
- Si faltan datos o no son válidos: 422 con errores[] por campo y no se crea nada. Con
"guardar_incompleto": truese guarda sin emitir para que la oficina lo complete en la bandeja. - El PDF imprime el juego de caracteres de Europa occidental (acentos, eñe, ç, €, «»). Letras como ń, Ł o Ş, o la flecha →, se rechazan con 422 texto.no_imprimible: el DeCA tiene que decir exactamente lo que mandas.
- El PDF y el QR:
url_publicaes la única URL que se imprime o se manda al conductor; no pide clave.qr_png,qr_svgypdfson de la API y piden la clave en la cabecera. - Para dejar muchos servicios de una vez está POST /servicios/lote (hasta 500, respuesta 207 con un resultado por elemento). El lote solo crea filas: se emiten desde el panel o una a una con POST /servicios/{id}/emitir.
Campos del servicio
Los datos del art. 6 de la Orden FOM/2861/2012 con los mismos nombres en la petición y en la respuesta. La última columna dice de quién es el dato según el art. 7; DecaDriver no traslada esa responsabilidad a nadie.
| Campo | Art. 6 | Obligatorio | Formato | Responde (art. 7) |
|---|---|---|---|---|
| referencia | — | No | ReferenciaTu referencia (n.º de pedido o expediente). Base de la idempotencia. Sin ella se asigna «DD-{id}» y no hay idempotencia. | — |
| cargador_razon | a) | Sí | Cargador: razón social | Cargador contractual |
| cargador_nif | a) | Sí | Cargador: NIFNIF, NIE o CIF con letra de control comprobada; NIF-IVA si es extranjero (con el país). | Cargador contractual |
| cargador_nif_pais | a) | No | Cargador: país del NIFPor defecto: ES. | Cargador contractual |
| cargador_domicilio | a) | Sí | Cargador: domicilio | Cargador contractual |
| transportista_razon | b) | Sí | Transportista: razón social | Cargador contractual |
| transportista_nif | b) | Sí | Transportista: NIF | Cargador contractual |
| transportista_nif_pais | b) | No | Transportista: país del NIFPor defecto: ES. | Cargador contractual |
| origen_texto | c) | Sí | Origen | Cargador contractual |
| destino_texto | c) | Sí | Destino | Cargador contractual |
| mercancia_naturaleza | d) | Sí | Naturaleza de la mercancía | Cargador contractual |
| peso_kg | d) | Sí, salvo peso_no_determinable | Peso (kg)Kilos. «12500», «12.500» y «12500,5» se leen como kilos; «12,5 t» es un error. Obligatorio salvo peso_no_determinable. | Cargador contractual |
| peso_no_determinable | d) | No | Peso no determinablePor defecto: false. | Cargador contractual |
| magnitud | d) | Si peso_no_determinable | Magnitud alternativaMagnitud alternativa cuando el peso no se puede determinar (art. 6 d). | Cargador contractual |
| cantidad | d) | No | CantidadCantidad si se conoce: «32 palés» o { "valor": 32, "unidad": "palés" }. | Cargador contractual |
| matricula_tractora | g) | Sí | Matrícula de la tractoraSe normaliza a «1234BCD». Extranjera con matricula_pais. | Transportista efectivo |
| matricula_remolque | g) | Sí, salvo sin_remolque | Matrícula del remolqueObligatoria salvo sin_remolque: true (vacío no distingue olvido de rígido). | Transportista efectivo |
| sin_remolque | g) | No | Sin remolquePor defecto: false. | Transportista efectivo |
| matricula_pais | g) | No | País de las matrículasPor defecto: ES. | Transportista efectivo |
| fecha_transporte | f) | Sí | Fecha del transporteAAAA-MM-DD (también DD/MM/AAAA). | Transportista efectivo |
| hora_prevista | — | No | Hora previstaHH:MM. Ordena la lista del conductor. | — |
| autorizacion_especial | e) | No | Autorización especial | Transportista efectivo |
| observaciones | h) | No | ObservacionesVan al PDF, que es público para quien tenga el QR: sin teléfonos ni nombres de personas. | Quien lo incluye |
| conductor | — | No | Teléfono de un conductor activo de tu empresa, o un objeto con uno solo de telefono, email o conductor_id. No es dato del art. 6: no va al PDF. Nunca crea conductores. | — |
| emitir | — | No | true (por defecto) emite en la misma llamada; false deja la fila sin emitir. | — |
| guardar_incompleto | — | No | true guarda aunque falten datos, sin emitir, para completarlo en el panel. | — |
- Fechas en AAAA-MM-DD (también DD/MM/AAAA). Una fecha pasada se admite con aviso y el PDF queda marcado «emitido después de la fecha prevista».
- NIF, NIE o CIF con o sin guiones: se comprueba la letra de control. Un NIF-IVA extranjero necesita el país (cargador_nif_pais).
- Matrículas en cualquier formato habitual: se guardan como «1234BCD». Extranjeras, con matricula_pais.
- Peso en kilos: «12500», «12.500» y «12500,5» valen; «12,5 t» es un error. Más de 40.000 kg da aviso; más de 60.000, error.
- Remolque vacío es un error salvo sin_remolque: true. El PDF escribe «Vehículo rígido, sin remolque» para que nadie lo lea como un olvido.
- Observaciones y motivos van al PDF, que ve cualquiera con el QR: sin teléfonos ni nombres de personas.
- Un campo que no existe se ignora con el aviso campo.desconocido; no hace fallar la petición.
Códigos de error y avisos
Un error siempre trae un código estable en «error», un mensaje en castellano y el request_id. Los avisos nunca bloquean: se emite igual.
HTTP/1.1 422 Unprocessable Entity
{
"ok": false,
"error": "servicio.datos_invalidos",
"mensaje": "Hay datos que faltan o no son válidos. No se ha creado nada.",
"errores": [
{
"campo": "cargador_nif",
"codigo": "cargador.nif_letra_control",
"mensaje": "La letra de control del NIF del cargador no cuadra.",
"responsable": "cargador",
"ayuda": "Dato del que responde el cargador contractual, según el art. 7 de la Orden FOM/2861/2012."
},
{
"campo": "matricula_remolque",
"codigo": "vehiculo.remolque_ausente",
"mensaje": "Falta la matrícula del remolque. Si el vehículo es rígido, marca «sin remolque».",
"responsable": "transportista",
"ayuda": "Dato del que responde el transportista efectivo, según el art. 7 de la Orden FOM/2861/2012."
}
],
"avisos": [],
"request_id": "req_Hx4p0LqWz8cV2mNb"
}Errores de los datos (dentro de errores[])
Cada uno lleva el campo, el responsable del dato según el art. 7 y una ayuda con ese texto.
| Código | Mensaje |
|---|---|
| cargador.razon_obligatoria | Falta la razón social del cargador contractual. |
| cargador.nif_obligatorio | Falta el NIF del cargador contractual. |
| cargador.nif_invalido | El NIF del cargador no tiene un formato válido. |
| cargador.nif_letra_control | La letra de control del NIF del cargador no cuadra. |
| cargador.nif_pais_invalido | El país del NIF del cargador no se reconoce (código ISO de dos letras). |
| cargador.domicilio_obligatorio | Falta el domicilio del cargador contractual. |
| transportista.razon_obligatoria | Falta la razón social del transportista efectivo. |
| transportista.nif_obligatorio | Falta el NIF del transportista efectivo. |
| transportista.nif_invalido | El NIF del transportista no tiene un formato válido. |
| transportista.nif_letra_control | La letra de control del NIF del transportista no cuadra. |
| transportista.nif_pais_invalido | El país del NIF del transportista no se reconoce (código ISO de dos letras). |
| origen.obligatorio | Falta el lugar de origen. |
| destino.obligatorio | Falta el lugar de destino. |
| mercancia.naturaleza_obligatoria | Falta la naturaleza de la mercancía. |
| peso.obligatorio | Falta el peso en kilos. Si no se puede determinar, marca «peso no determinable» e indica la magnitud. |
| peso.formato | El peso no se entiende. Escribe kilos: «12500», «12.500» o «12500,5». |
| peso.unidad | El peso parece estar en toneladas. Escríbelo en kilos. |
| peso.maximo | El peso supera los 60.000 kg. Comprueba la cifra. |
| peso.cero | El peso tiene que ser mayor que cero. |
| peso.magnitud_obligatoria | Si el peso no se puede determinar, indica la magnitud alternativa (art. 6 d). |
| vehiculo.tractora_obligatoria | Falta la matrícula de la tractora. |
| vehiculo.tractora_formato | La matrícula de la tractora no tiene un formato válido («1234 BCD»). Si es extranjera, indica el país. |
| vehiculo.remolque_ausente | Falta la matrícula del remolque. Si el vehículo es rígido, marca «sin remolque». |
| vehiculo.remolque_formato | La matrícula del remolque no tiene un formato válido («R 1234 BCD» o «1234 BCD»). |
| vehiculo.remolque_contradictorio | Hay matrícula de remolque y a la vez «sin remolque». Deja solo una de las dos. |
| vehiculo.pais_invalido | El país de las matrículas no se reconoce (código ISO de dos letras). |
| fecha.obligatoria | Falta la fecha del transporte. |
| fecha.formato | La fecha no se entiende. Usa DD/MM/AAAA o AAAA-MM-DD. |
| fecha.invalida | La fecha no existe en el calendario. |
| hora.formato | La hora prevista no se entiende. Usa HH:MM. |
| texto.demasiado_largo | El texto es demasiado largo. |
| texto.no_imprimible | El texto lleva caracteres que el PDF del DeCA no puede imprimir. Escríbelos sin ese signo o con otra grafía. |
| cantidad.no_positiva | La cantidad tiene que ser mayor que cero. |
| referencia.formato | La referencia solo admite letras, números, guiones, barras, puntos y espacios (máximo 100), y no puede empezar por guion. |
| campo.tipo | El valor tiene un tipo que no se admite. |
Errores de la API
| Código | HTTP | Cuándo |
|---|---|---|
| clave.ausente | 401 | Falta la cabecera Authorization: Bearer. |
| clave.invalida | 401 | La clave no existe, está revocada o no es del entorno que dice su prefijo. |
| clave.en_url | 400 | La clave aparece en la URL. Va solo en la cabecera; sustitúyela por si ha quedado en algún registro. |
| empresa.inactiva | 403 | La empresa está suspendida o de baja. Los DeCA emitidos siguen accesibles por su QR. |
| limite.peticiones | 429 | Más de 120 peticiones por minuto con la misma clave, o demasiados intentos con claves no válidas. Respeta Retry-After. |
| peticion.demasiado_grande | 413 | El cuerpo supera los 256 KB. |
| peticion.json_invalido | 400 | El cuerpo no es JSON válido en UTF-8. |
| peticion.cuerpo | 400 | El cuerpo no es un objeto JSON (o una lista, en el lote). |
| peticion.cuerpo_vacio | 400 | Falta el cuerpo (también en /anular, que necesita tipo y motivo). |
| peticion.longitud | 400 | La cabecera Content-Length no es un número. |
| parametro.* | 400 | Un parámetro de la URL no es válido (fecha, estado, modificado_desde, cursor, limite, mes, version). |
| no_encontrado | 404 | El servicio no existe, es de otra empresa o del otro entorno. Siempre el mismo 404. |
| ruta.no_encontrada | 404 | La ruta no existe en la API v1. |
| metodo.no_permitido | 405 | La ruta existe pero no admite ese método (la cabecera Allow dice cuáles sí). |
| servicio.datos_invalidos | 422 | Faltan datos o no son válidos. No se ha creado ni cambiado nada. Detalle en errores[]. |
| servicio.difiere_del_emitido | 409 | Ya hay un DeCA emitido con esa referencia y el cuerpo no coincide. Detalle en diferencias[]; usa /modificaciones con motivo. |
| servicio.emitiendo | 409 | El servicio se está emitiendo en otra petición. Reintenta en unos segundos. |
| servicio.emision_fallida | 503 | Guardado pero sin PDF por un fallo temporal. Reintenta la misma petición: no se duplica. |
| servicio.incompleto | 409 | /emitir sobre un servicio con datos incompletos. Detalle en errores[]. |
| servicio.no_emitido | 409 | Se pide el PDF o el QR de un servicio que aún no está emitido. |
| servicio.no_modificable | 409 | El servicio no admite modificaciones en su estado (borrador o anulado). |
| servicio.estado_cambiado | 409 | El servicio cambió de estado a la vez que tu petición. Vuelve a leerlo. |
| transicion_invalida | 409 | La acción no es posible en el estado actual (p. ej. finalizar un anulado). |
| empresa.no_emite | 409 | La empresa no puede emitir en su estado actual. |
| version.inexistente | 409 | version_base es mayor que la versión vigente: esa versión no existe. Detalle en version_actual. |
| version.desfasada | 409 | Alguien cambió ese campo después de tu version_base. Detalle en conflictos[] con el valor actual. |
| modificacion.formato | 422 | cambios[] no tiene el formato { campo, valor } o trae campos desconocidos o repetidos. |
| modificacion.campo_desconocido | 422 | Un campo de cambios[] no existe. |
| modificacion.campo_repetido | 422 | Un campo aparece dos veces en cambios[]. |
| modificacion.motivo | 422 | Falta el motivo o tiene menos de 5 caracteres. Va al PDF. |
| modificacion.sin_campos | 422 | cambios[] está vacío. |
| modificacion.sin_cambios | 409 | Ningún valor cambia respecto al vigente. |
| modificacion.campo_no_editable | 422 | La referencia no forma parte del documento y no se modifica. |
| anulacion.ya_salio | 409 | «no_salio» sobre un transporte que ya salió (el conductor pulsó «Salgo») o terminó: anúlalo «en_ruta». |
| anulacion.tipo | 422 | tipo no es «no_salio» ni «en_ruta». |
| conductor.formato | 422 | «conductor» no es un teléfono ni un objeto con uno solo de telefono, email o conductor_id. |
| conductor.identificador | 422 | PUT /conductor sin uno (y solo uno) de telefono, email o conductor_id. |
| conductor.no_encontrado | 422 | PUT /conductor con un dato que no es de ningún conductor de tu empresa. |
| conductor.pendiente_confirmacion | 409 | El conductor aún no está confirmado. Se confirma en el panel, nunca desde la API. |
| conductor.no_activo | 409 | El conductor está de baja en la empresa. |
| conductor.no_es_de_pruebas | 409 | Un servicio de prueba solo se asigna a conductores marcados «de pruebas». |
| lote.formato | 422 | El lote no trae { "servicios": [ … ] }. |
| lote.vacio | 422 | El lote no trae ningún servicio. |
| lote.demasiados | 413 | Más de 500 servicios en un lote. |
| lote.demasiadas_actualizaciones | 413 | El lote actualizaría más de 100 borradores ya existentes. Divídelo. Detalle en actualizaciones y maximo. |
| peticion.elemento | 422 | En un elemento del lote: no es un objeto con los datos del servicio. |
| lote.referencia_repetida | 409 | En un elemento del lote: la referencia ya aparece antes en el mismo lote. |
| lote.no_guardado | 503 | En un elemento del lote: no se pudo guardar. Reintenta el lote entero. |
| pdf.no_disponible | 503 | El PDF no se puede leer ahora mismo. Reintenta. |
| error_interno | 500 | Error nuestro. Reintenta con la misma referencia y cita el request_id. |
Avisos (en avisos[], no bloquean)
| Código | Qué significa |
|---|---|
| fecha.improbable | Fecha improbable: está a más de 30 días atrás o 60 adelante. Compruébala. |
| fecha.pasada | La fecha del transporte ya ha pasado. Se puede emitir, y quedará la marca «emitido después de la fecha prevista». |
| peso.alto | El peso supera los 40.000 kg. Comprueba la cifra. |
| peso.ignorado | Has marcado «peso no determinable»: el peso en kilos se ignora. |
| campo.desconocido | Un campo del cuerpo no existe en el servicio: se ignora. |
| referencia.generada | Sin referencia: se asigna «DD-{id}» y no hay idempotencia. |
| conductor.no_encontrado | El conductor indicado no es un conductor activo de la empresa: queda sin asignar. |
| conductor.pendiente | El conductor aún no está confirmado: queda sin asignar. |
| conductor.no_es_de_pruebas | Servicio de prueba con un conductor que no es «de pruebas»: queda sin asignar. |
| servicio.no_emitido_incompleto | Con guardar_incompleto: true y datos que faltan, se guarda sin emitir. |
| lote.no_emite | El lote ignora emitir: true; solo crea filas. |
| lote.no_guarda_incompleto | El lote ignora guardar_incompleto: true; un elemento con datos que faltan responde 422. Mándalo con POST /v1/servicios. |
| texto.dato_personal | Las observaciones o el motivo parecen llevar un teléfono o un DNI. Van al PDF, público para quien tenga el QR. No bloquea. |
- 401: falta la clave o no vale. 404: el servicio no existe, es de otra empresa o del otro entorno (siempre el mismo 404, nunca 403).
- 429 y 503 traen Retry-After: espera esos segundos y reintenta la misma petición.
- Cuerpo máximo de 256 KB (413 si se pasa). Límite orientativo de 120 peticiones por minuto y clave.
Conductores
La API asigna servicios a conductores que ya están activos en tu empresa. Nunca crea conductores ni confirma solicitudes: eso se hace en el panel.
curl -s "https://decadriver.es/api/v1/conductores?estado=activo" \
-H "Authorization: Bearer $DECADRIVER_CLAVE"- Devuelve id, nombre, teléfono, estado (activo, pendiente, baja, bloqueado), matrícula habitual y si es externo. No guardamos DNI.
- Al crear,
conductorcasa por teléfono solo con conductores activos. Si no casa, el servicio queda sin asignar con el aviso conductor.no_encontrado o conductor.pendiente, y se emite igual. - Para asignar o cambiar después, PUT /servicios/{id}/conductor con telefono, email o conductor_id. El correo solo identifica a usuarios de oficina que también conducen: los conductores no tienen correo en DecaDriver.
- Un conductor que aún no está confirmado responde 409 conductor.pendiente_confirmacion. Se confirma en el panel para que quede quién lo hizo.
- Asignar o quitar el conductor no cambia el PDF: el conductor no es un dato del art. 6. DELETE /servicios/{id}/conductor lo deja sin conductor.
curl -s -X PUT https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/conductor \
-H "Authorization: Bearer $DECADRIVER_CLAVE" \
-H "Content-Type: application/json" \
-d '{ "telefono": "+34600000000" }'Modificar con motivo
Un DeCA emitido no se sobrescribe: se modifica con motivo. Cada modificación añade al mismo PDF, en la misma URL, los datos nuevos, el motivo y los anteriores marcados como no válidos.
curl -s https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/modificaciones \
-H "Authorization: Bearer $DECADRIVER_CLAVE" \
-H "Content-Type: application/json" \
-d '{
"cambios": [
{ "campo": "peso_kg", "valor": 12380 },
{ "campo": "cantidad", "valor": "31 palés" }
],
"motivo": "Peso de báscula en el muelle de carga",
"version_base": 1
}'- Varios campos, un motivo, una versión. El motivo es obligatorio (mínimo 5 caracteres) y aparece en el PDF: escribe el real.
- Si el motivo o las observaciones parecen llevar un teléfono o un DNI, la respuesta trae el aviso texto.dato_personal: el PDF es público para quien tenga el QR. No bloquea.
- El autor queda como «Sistema». No hay motivo por defecto: el que mandes es el que lee el agente en la inspección.
- version_base es la versión sobre la que propones el cambio. Si alguien (el conductor en ruta, la oficina) cambió ese mismo campo después, responde 409 version.desfasada con el valor actual; un campo distinto no es conflicto. Una version_base mayor que la vigente responde 409 version.inexistente.
- Se puede corregir un servicio finalizado; uno anulado ya no se modifica.
HTTP/1.1 409 Conflict
{
"ok": false,
"error": "version.desfasada",
"mensaje": "Alguien cambió ese dato después de tu versión.",
"version_actual": 2,
"conflictos": [
{ "campo": "peso_kg", "valor_actual": "12410", "autor_tipo": "conductor", "registrado_en": "2026-10-06T06:41:12.090Z", "version": 2 }
],
"request_id": "req_Vb6n2KsPq9dX1zMe"
}Anular: POST /servicios/{id}/anular con tipo «no_salio» o «en_ruta» y motivo. El PDF se regenera con la marca ANULADO en la misma URL, que sigue sirviendo. Un anulado «no salió» no cuenta para el consumo del mes mientras el mes no esté cerrado; uno «en ruta», sí.
curl -s https://decadriver.es/api/v1/servicios/Yq3m0bZr8TfQ2nXa/anular \
-H "Authorization: Bearer $DECADRIVER_CLAVE" \
-H "Content-Type: application/json" \
-d '{ "tipo": "no_salio", "motivo": "El cliente canceló la carga" }'Emitir un borrador: POST /servicios/{id}/emitir (repetirlo no emite dos veces; sobre un anulado responde 409 transicion_invalida). Fijar el fin: POST /servicios/{id}/finalizar.
Cuando el POST /servicios responde 409 servicio.difiere_del_emitido, las diferencias te dicen qué campos cambiarías:
HTTP/1.1 409 Conflict
{
"ok": false,
"error": "servicio.difiere_del_emitido",
"mensaje": "Ya hay un DeCA emitido con esa referencia y los datos no coinciden. …",
"servicio_id": "Yq3m0bZr8TfQ2nXa",
"numero": "PRUEBA-000001",
"version": 1,
"diferencias": [
{ "campo": "peso_kg", "etiqueta": "Peso (kg)", "valor_deca": "12500", "valor_peticion": "12380" }
],
"request_id": "req_Rz9k3NfYb1tQ6wLa"
}Saber qué ha cambiado
Un solo bucle recoge todo: GET /servicios?modificado_desde= devuelve cada servicio que ha tenido una versión nueva, una anulación, una asignación, una entrega o un fin, con sus modificaciones.
curl -s "https://decadriver.es/api/v1/servicios?modificado_desde=2026-10-06T05:59:00Z&limite=500" \
-H "Authorization: Bearer $DECADRIVER_CLAVE"- Orden ascendente por modificado_en. Pagina con siguiente_cursor mientras hay_mas sea true.
- modificado_desde va en ISO 8601 con zona: «2026-10-06T05:59:00Z» o «2026-10-06T07:59:00+02:00», con el «+» codificado como %2B (lo hacen solos requests, httpx o
curl --data-urlencode). Una hora sin zona responde 400: sería ambigua. - Usa nuestra hora como punto de partida: servidor_ahora de la primera página. La próxima vez pide desde esa hora menos 60 segundos.
- Por ese minuto que se repite, deduplica por id de modificación: cada modificación trae su id, versión, campos (anterior y nuevo), motivo y autor.
- El autor es un rol (conductor, oficina o sistema), nunca un nombre. Los cambios del conductor en ruta llegan por aquí igual que los de la oficina.
- Cada cinco minutos es una frecuencia razonable. Los avisos automáticos (webhooks) llegarán después; este bucle seguirá sirviendo de red de seguridad.
import os
import time
from datetime import datetime, timedelta
import requests
API = "https://decadriver.es/api/v1"
CABECERAS = {"Authorization": f"Bearer {os.environ['DECADRIVER_CLAVE']}"}
def sincronizar(ultimo_servidor_ahora):
"""Trae todo lo que ha cambiado desde la última vez.
Devuelve el punto de partida de la próxima vuelta: guárdalo en tu base
de datos junto con lo procesado.
"""
if ultimo_servidor_ahora:
# Se resta un minuto para no perder nada que se escribiera
# mientras respondíamos la consulta anterior.
inicio = datetime.fromisoformat(ultimo_servidor_ahora.replace("Z", "+00:00"))
desde = (inicio - timedelta(seconds=60)).isoformat()
else:
desde = "1970-01-01T00:00:00Z"
params = {"modificado_desde": desde, "limite": 500}
siguiente = None
while True:
r = requests.get(f"{API}/servicios", headers=CABECERAS, params=params, timeout=30)
if r.status_code in (429, 503):
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
pagina = r.json()
# La hora del servidor de la PRIMERA página es el próximo punto de partida.
siguiente = siguiente or pagina["servidor_ahora"]
for servicio in pagina["servicios"]:
guardar_servicio(servicio) # upsert por servicio["id"]: estado, versión, conductor, datos
for m in servicio["modificaciones"]:
if not modificacion_ya_procesada(m["id"]): # por el minuto que se repite
procesar_modificacion(servicio, m)
if not pagina["hay_mas"]:
return siguiente
params = {"cursor": pagina["siguiente_cursor"], "limite": 500}Para un servicio concreto: GET /servicios/{id} trae además versiones[] con su fecha, su hash SHA-256 y el PDF de cada versión tal como se sirvió (GET /servicios/{id}/pdf?version=n). Para buscar por tu referencia: GET /servicios?referencia=PED-0417.
Entorno de pruebas
Con la clave dd_test_ todo es de prueba: nada de lo que hagas cuenta, se factura ni vale como documento.
- Número de la serie PRUEBA-000001, URL /d/prueba/… y la marca «PRUEBA – SIN VALIDEZ» en el PDF, en el pie, bajo el QR y en el nombre del fichero.
- Un servicio de prueba nunca se convierte en real. Con la clave de prueba, un id real responde 404, y al revés.
- Solo se asignan a conductores marcados «de pruebas» en el panel (GET /conductores con la clave de prueba solo enseña esos), para que un conductor real nunca lleve un documento de prueba en el móvil.
- GET /consumo con la clave de prueba cuenta los de prueba, con cuenta_para_facturacion: false.
- Usa datos ficticios: los servicios de prueba no se conservan y se purgarán a los 30 días. Usar un documento de prueba en un transporte real no cumple la obligación y es responsabilidad de quien lo hace.
Importar un CSV
Si tu sistema no puede llamar a la API, sube su exportación al panel: pasa por el mismo validador y da los mismos códigos de error.
- En el panel, Envíos → Importar: Excel (.xlsx), CSV o TXT (separador punto y coma, coma o tabulador; UTF-8 o Windows-1252). La primera vez asignas tus columnas a los campos de arriba y DecaDriver recuerda el mapeo.
- Un .xls antiguo no se lee: guárdalo antes como .xlsx o como CSV desde Excel.
- Las filas con errores no se emiten; puedes descargarlas con una columna de errores, corregirlas y volver a subirlas. La referencia evita duplicados.
- Desde un programa sin navegador, usa POST /servicios/lote con los mismos objetos que el POST individual.
Lista de comprobación
Antes de pasar a la clave real, repasa esto con tu integración funcionando contra la de prueba.
- La clave está en un gestor de secretos del servidor. Nunca en el código, en un navegador, en una app móvil ni en una URL.
- Cada servicio viaja con tu referencia, y ante un corte, un 5xx o un 429 reintentas la misma petición (respetando Retry-After).
- Un 422 enseña errores[] a quien puede corregir el dato, con el responsable según el art. 7.
- Un 409 servicio.difiere_del_emitido no se reintenta: se registra como modificación con un motivo real.
- Guardas id, numero y url_publica de cada servicio. La url_publica (o su QR) es lo que se imprime o se envía; nada más.
- Los motivos y las observaciones no llevan teléfonos ni nombres de personas: son públicos para quien tenga el QR.
- El bucle de sincronización usa servidor_ahora menos 60 segundos y deduplica por id de modificación.
- Registras el X-Request-Id de cada respuesta con error.
- Has probado con dd_test_ un alta, una repetición, un error de datos, una modificación con version_base y una anulación.
- Los conductores están dados de alta y activos en el panel: la API no los crea.
- Con dd_live_, el primer servicio real lo emites controlado y compruebas el PDF en la url_publica.
Ejemplos en curl, Python, PHP y C#
Crear y emitir un servicio, y tratar la respuesta: correcta, con errores por campo o con un error de la API.
curl
# servicio.json es el cuerpo de arriba
curl -s https://decadriver.es/api/v1/servicios \
-H "Authorization: Bearer $DECADRIVER_CLAVE" \
-H "Content-Type: application/json" \
--data-binary @servicio.jsonPython
import os
import requests
API = "https://decadriver.es/api/v1"
CABECERAS = {"Authorization": f"Bearer {os.environ['DECADRIVER_CLAVE']}"}
servicio = {
"referencia": "PED-0417",
"cargador_razon": "Conservas Ejemplo SL",
"cargador_nif": "B12345674",
"cargador_domicilio": "Polígono Norte, nave 4, Lugo",
"transportista_razon": "Transportes Ejemplo SL",
"transportista_nif": "B87654323",
"origen_texto": "Lugo",
"destino_texto": "Vigo, puerto",
"mercancia_naturaleza": "Conservas en palés",
"peso_kg": 12500,
"matricula_tractora": "1234 BCD",
"matricula_remolque": "R 5678 FGH",
"fecha_transporte": "2026-10-06",
}
r = requests.post(f"{API}/servicios", json=servicio, headers=CABECERAS, timeout=30)
datos = r.json()
if r.status_code in (200, 201):
print(datos["numero"], datos["url_publica"])
for aviso in datos["avisos"]:
print("Aviso:", aviso["mensaje"])
elif r.status_code == 422:
for e in datos["errores"]:
print(f'{e["campo"]}: {e["mensaje"]} ({e["responsable"]})')
else:
print("Error", r.status_code, datos.get("error"), r.headers.get("X-Request-Id"))PHP
<?php
$api = 'https://decadriver.es/api/v1';
$servicio = [
'referencia' => 'PED-0417',
'cargador_razon' => 'Conservas Ejemplo SL',
'cargador_nif' => 'B12345674',
'cargador_domicilio' => 'Polígono Norte, nave 4, Lugo',
'transportista_razon' => 'Transportes Ejemplo SL',
'transportista_nif' => 'B87654323',
'origen_texto' => 'Lugo',
'destino_texto' => 'Vigo, puerto',
'mercancia_naturaleza' => 'Conservas en palés',
'peso_kg' => 12500,
'matricula_tractora' => '1234 BCD',
'matricula_remolque' => 'R 5678 FGH',
'fecha_transporte' => '2026-10-06',
];
$ch = curl_init("$api/servicios");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DECADRIVER_CLAVE'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($servicio, JSON_UNESCAPED_UNICODE),
]);
$cuerpo = curl_exec($ch);
$estado = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$datos = json_decode($cuerpo, true);
if ($estado === 200 || $estado === 201) {
echo $datos['numero'], ' ', $datos['url_publica'], PHP_EOL;
} elseif ($estado === 422) {
foreach ($datos['errores'] as $e) {
echo "{$e['campo']}: {$e['mensaje']}", PHP_EOL;
}
} else {
echo "Error $estado: {$datos['error']} ({$datos['request_id']})", PHP_EOL;
}C#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var api = "https://decadriver.es/api/v1";
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("DECADRIVER_CLAVE"));
var servicio = new Dictionary<string, object>
{
["referencia"] = "PED-0417",
["cargador_razon"] = "Conservas Ejemplo SL",
["cargador_nif"] = "B12345674",
["cargador_domicilio"] = "Polígono Norte, nave 4, Lugo",
["transportista_razon"] = "Transportes Ejemplo SL",
["transportista_nif"] = "B87654323",
["origen_texto"] = "Lugo",
["destino_texto"] = "Vigo, puerto",
["mercancia_naturaleza"] = "Conservas en palés",
["peso_kg"] = 12500,
["matricula_tractora"] = "1234 BCD",
["matricula_remolque"] = "R 5678 FGH",
["fecha_transporte"] = "2026-10-06",
};
var respuesta = await http.PostAsJsonAsync($"{api}/servicios", servicio);
using var datos = JsonDocument.Parse(await respuesta.Content.ReadAsStringAsync());
var raiz = datos.RootElement;
if (respuesta.IsSuccessStatusCode)
{
Console.WriteLine($"{raiz.GetProperty("numero")} {raiz.GetProperty("url_publica")}");
}
else if ((int)respuesta.StatusCode == 422)
{
foreach (var e in raiz.GetProperty("errores").EnumerateArray())
Console.WriteLine($"{e.GetProperty("campo")}: {e.GetProperty("mensaje")}");
}
else
{
var requestId = respuesta.Headers.TryGetValues("X-Request-Id", out var v) ? v.First() : "";
Console.WriteLine($"Error {(int)respuesta.StatusCode}: {raiz.GetProperty("error")} ({requestId})");
}La referencia completa, con todos los esquemas, está en openapi.json (OpenAPI 3.1): se puede importar en Postman, Insomnia o en un generador de clientes.
Todas las llamadas
Todas cuelgan de https://decadriver.es/api/v1 y piden la clave en la cabecera Authorization.
| Método | Ruta | Qué hace |
|---|---|---|
| GET | /yo | Empresa, entorno y prefijo de la clave. |
| POST | /servicios | Crea y, por defecto, emite. Idempotente por referencia. |
| POST | /servicios/lote | Hasta 500 servicios sin emitir; 207 por elemento. |
| GET | /servicios | ?referencia=, ?fecha=&estado=, ?modificado_desde=&cursor=&limite=. |
| GET | /servicios/{id} | Estado, versión, conductor, modificaciones y versiones. |
| PUT | /servicios/{id}/conductor | Asigna a un conductor activo (telefono, email o conductor_id). |
| DELETE | /servicios/{id}/conductor | Deja el servicio sin conductor. |
| POST | /servicios/{id}/modificaciones | cambios[], motivo y version_base. Nueva versión del PDF. |
| POST | /servicios/{id}/emitir | Emite un borrador. |
| POST | /servicios/{id}/anular | Anula con tipo y motivo; el PDF lleva la marca. |
| POST | /servicios/{id}/finalizar | Fija el fin real del servicio. |
| GET | /servicios/{id}/pdf | PDF vigente o ?version=n, con la clave. |
| GET | /servicios/{id}/qr.svg · qr.png | El QR (solo codifica la URL pública), con la clave. |
| GET | /conductores | Conductores de la empresa, ?estado=. |
| GET | /consumo | DeCA emitidos en un mes, ?mes=AAAA-MM. |
Estado del producto
La API v1 descrita arriba es la que hay construida. Estas piezas llegan después y no están disponibles todavía:
- Avisos automáticos (webhooks) de servicio emitido y modificado, firmados con HMAC y con reintentos. Hasta entonces, el bucle de sincronización.
- Lote con emisión en cola y POST /v1/importaciones para mandar un CSV con un mapeo guardado.
- Límite de peticiones por clave con cabeceras de cuota y política de versiones publicada.
- Registro de llamadas de 30 días visible en el panel.
- Lectura directa de ficheros .xlsx en la importación.
- Purga automática de los servicios de prueba a los 30 días.
Si vas a integrar tu sistema, cuéntanos cuál es: lo revisamos con vosotros y lo probamos con servicios reales antes de pasar a la clave real.
¿Todavía no sabes si te hace falta la API? Empieza por cómo se integra el DeCA con tu sistema.