DecaDriver

API v1 · REST en JSON

Documentación de la API de DecaDriver

Actualizado el 29 de septiembre de 2026

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:

GET /yo
curl -s https://decadriver.es/api/v1/yo \
  -H "Authorization: Bearer $DECADRIVER_CLAVE"
200 OK
{
  "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.

  1. Crea la clave de prueba en Ajustes → API y ponla en la variable de entorno DECADRIVER_CLAVE.
  2. Llama a GET /yo: tiene que responder con tu empresa y «entorno»: «test».
  3. Manda el servicio con POST /servicios (cuerpo de abajo, datos ficticios).
  4. Abre url_publica: es el PDF, con la marca «PRUEBA – SIN VALIDEZ». Es la URL que va en el QR.
  5. Repite exactamente la misma petición: responde 200 con X-Idempotent-Replay: true y el mismo servicio. No se ha creado otro.
POST /servicios · cuerpo
{
  "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" }
}
Respuesta (resumida)
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": false la 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": true se 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_publica es la única URL que se imprime o se manda al conductor; no pide clave. qr_png, qr_svg y pdf son 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.

Campos que acepta POST /servicios (y devuelve «datos»)
CampoArt. 6ObligatorioFormatoResponde (art. 7)
referencia—NoReferenciaTu referencia (n.º de pedido o expediente). Base de la idempotencia. Sin ella se asigna «DD-{id}» y no hay idempotencia.—
cargador_razona)SíCargador: razón socialCargador contractual
cargador_nifa)SíCargador: NIFNIF, NIE o CIF con letra de control comprobada; NIF-IVA si es extranjero (con el país).Cargador contractual
cargador_nif_paisa)NoCargador: país del NIFPor defecto: ES.Cargador contractual
cargador_domicilioa)SíCargador: domicilioCargador contractual
transportista_razonb)SíTransportista: razón socialCargador contractual
transportista_nifb)SíTransportista: NIFCargador contractual
transportista_nif_paisb)NoTransportista: país del NIFPor defecto: ES.Cargador contractual
origen_textoc)SíOrigenCargador contractual
destino_textoc)SíDestinoCargador contractual
mercancia_naturalezad)SíNaturaleza de la mercancíaCargador contractual
peso_kgd)Sí, salvo peso_no_determinablePeso (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_determinabled)NoPeso no determinablePor defecto: false.Cargador contractual
magnitudd)Si peso_no_determinableMagnitud alternativaMagnitud alternativa cuando el peso no se puede determinar (art. 6 d).Cargador contractual
cantidadd)NoCantidadCantidad si se conoce: «32 palés» o { "valor": 32, "unidad": "palés" }.Cargador contractual
matricula_tractorag)SíMatrícula de la tractoraSe normaliza a «1234BCD». Extranjera con matricula_pais.Transportista efectivo
matricula_remolqueg)Sí, salvo sin_remolqueMatrícula del remolqueObligatoria salvo sin_remolque: true (vacío no distingue olvido de rígido).Transportista efectivo
sin_remolqueg)NoSin remolquePor defecto: false.Transportista efectivo
matricula_paisg)NoPaís de las matrículasPor defecto: ES.Transportista efectivo
fecha_transportef)SíFecha del transporteAAAA-MM-DD (también DD/MM/AAAA).Transportista efectivo
hora_prevista—NoHora previstaHH:MM. Ordena la lista del conductor.—
autorizacion_especiale)NoAutorización especialTransportista efectivo
observacionesh)NoObservacionesVan al PDF, que es público para quien tenga el QR: sin teléfonos ni nombres de personas.Quien lo incluye
conductor—NoTelé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—Notrue (por defecto) emite en la misma llamada; false deja la fila sin emitir.—
guardar_incompleto—Notrue 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.

422 · datos que faltan o no son válidos
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ódigos de validación de los datos del DeCA
CódigoMensaje
cargador.razon_obligatoriaFalta la razón social del cargador contractual.
cargador.nif_obligatorioFalta el NIF del cargador contractual.
cargador.nif_invalidoEl NIF del cargador no tiene un formato válido.
cargador.nif_letra_controlLa letra de control del NIF del cargador no cuadra.
cargador.nif_pais_invalidoEl país del NIF del cargador no se reconoce (código ISO de dos letras).
cargador.domicilio_obligatorioFalta el domicilio del cargador contractual.
transportista.razon_obligatoriaFalta la razón social del transportista efectivo.
transportista.nif_obligatorioFalta el NIF del transportista efectivo.
transportista.nif_invalidoEl NIF del transportista no tiene un formato válido.
transportista.nif_letra_controlLa letra de control del NIF del transportista no cuadra.
transportista.nif_pais_invalidoEl país del NIF del transportista no se reconoce (código ISO de dos letras).
origen.obligatorioFalta el lugar de origen.
destino.obligatorioFalta el lugar de destino.
mercancia.naturaleza_obligatoriaFalta la naturaleza de la mercancía.
peso.obligatorioFalta el peso en kilos. Si no se puede determinar, marca «peso no determinable» e indica la magnitud.
peso.formatoEl peso no se entiende. Escribe kilos: «12500», «12.500» o «12500,5».
peso.unidadEl peso parece estar en toneladas. Escríbelo en kilos.
peso.maximoEl peso supera los 60.000 kg. Comprueba la cifra.
peso.ceroEl peso tiene que ser mayor que cero.
peso.magnitud_obligatoriaSi el peso no se puede determinar, indica la magnitud alternativa (art. 6 d).
vehiculo.tractora_obligatoriaFalta la matrícula de la tractora.
vehiculo.tractora_formatoLa matrícula de la tractora no tiene un formato válido («1234 BCD»). Si es extranjera, indica el país.
vehiculo.remolque_ausenteFalta la matrícula del remolque. Si el vehículo es rígido, marca «sin remolque».
vehiculo.remolque_formatoLa matrícula del remolque no tiene un formato válido («R 1234 BCD» o «1234 BCD»).
vehiculo.remolque_contradictorioHay matrícula de remolque y a la vez «sin remolque». Deja solo una de las dos.
vehiculo.pais_invalidoEl país de las matrículas no se reconoce (código ISO de dos letras).
fecha.obligatoriaFalta la fecha del transporte.
fecha.formatoLa fecha no se entiende. Usa DD/MM/AAAA o AAAA-MM-DD.
fecha.invalidaLa fecha no existe en el calendario.
hora.formatoLa hora prevista no se entiende. Usa HH:MM.
texto.demasiado_largoEl texto es demasiado largo.
texto.no_imprimibleEl texto lleva caracteres que el PDF del DeCA no puede imprimir. Escríbelos sin ese signo o con otra grafía.
cantidad.no_positivaLa cantidad tiene que ser mayor que cero.
referencia.formatoLa referencia solo admite letras, números, guiones, barras, puntos y espacios (máximo 100), y no puede empezar por guion.
campo.tipoEl valor tiene un tipo que no se admite.

Errores de la API

Código de «error», estado HTTP y cuándo aparece
CódigoHTTPCuándo
clave.ausente401Falta la cabecera Authorization: Bearer.
clave.invalida401La clave no existe, está revocada o no es del entorno que dice su prefijo.
clave.en_url400La clave aparece en la URL. Va solo en la cabecera; sustitúyela por si ha quedado en algún registro.
empresa.inactiva403La empresa está suspendida o de baja. Los DeCA emitidos siguen accesibles por su QR.
limite.peticiones429Más de 120 peticiones por minuto con la misma clave, o demasiados intentos con claves no válidas. Respeta Retry-After.
peticion.demasiado_grande413El cuerpo supera los 256 KB.
peticion.json_invalido400El cuerpo no es JSON válido en UTF-8.
peticion.cuerpo400El cuerpo no es un objeto JSON (o una lista, en el lote).
peticion.cuerpo_vacio400Falta el cuerpo (también en /anular, que necesita tipo y motivo).
peticion.longitud400La cabecera Content-Length no es un número.
parametro.*400Un parámetro de la URL no es válido (fecha, estado, modificado_desde, cursor, limite, mes, version).
no_encontrado404El servicio no existe, es de otra empresa o del otro entorno. Siempre el mismo 404.
ruta.no_encontrada404La ruta no existe en la API v1.
metodo.no_permitido405La ruta existe pero no admite ese método (la cabecera Allow dice cuáles sí).
servicio.datos_invalidos422Faltan datos o no son válidos. No se ha creado ni cambiado nada. Detalle en errores[].
servicio.difiere_del_emitido409Ya hay un DeCA emitido con esa referencia y el cuerpo no coincide. Detalle en diferencias[]; usa /modificaciones con motivo.
servicio.emitiendo409El servicio se está emitiendo en otra petición. Reintenta en unos segundos.
servicio.emision_fallida503Guardado pero sin PDF por un fallo temporal. Reintenta la misma petición: no se duplica.
servicio.incompleto409/emitir sobre un servicio con datos incompletos. Detalle en errores[].
servicio.no_emitido409Se pide el PDF o el QR de un servicio que aún no está emitido.
servicio.no_modificable409El servicio no admite modificaciones en su estado (borrador o anulado).
servicio.estado_cambiado409El servicio cambió de estado a la vez que tu petición. Vuelve a leerlo.
transicion_invalida409La acción no es posible en el estado actual (p. ej. finalizar un anulado).
empresa.no_emite409La empresa no puede emitir en su estado actual.
version.inexistente409version_base es mayor que la versión vigente: esa versión no existe. Detalle en version_actual.
version.desfasada409Alguien cambió ese campo después de tu version_base. Detalle en conflictos[] con el valor actual.
modificacion.formato422cambios[] no tiene el formato { campo, valor } o trae campos desconocidos o repetidos.
modificacion.campo_desconocido422Un campo de cambios[] no existe.
modificacion.campo_repetido422Un campo aparece dos veces en cambios[].
modificacion.motivo422Falta el motivo o tiene menos de 5 caracteres. Va al PDF.
modificacion.sin_campos422cambios[] está vacío.
modificacion.sin_cambios409Ningún valor cambia respecto al vigente.
modificacion.campo_no_editable422La referencia no forma parte del documento y no se modifica.
anulacion.ya_salio409«no_salio» sobre un transporte que ya salió (el conductor pulsó «Salgo») o terminó: anúlalo «en_ruta».
anulacion.tipo422tipo no es «no_salio» ni «en_ruta».
conductor.formato422«conductor» no es un teléfono ni un objeto con uno solo de telefono, email o conductor_id.
conductor.identificador422PUT /conductor sin uno (y solo uno) de telefono, email o conductor_id.
conductor.no_encontrado422PUT /conductor con un dato que no es de ningún conductor de tu empresa.
conductor.pendiente_confirmacion409El conductor aún no está confirmado. Se confirma en el panel, nunca desde la API.
conductor.no_activo409El conductor está de baja en la empresa.
conductor.no_es_de_pruebas409Un servicio de prueba solo se asigna a conductores marcados «de pruebas».
lote.formato422El lote no trae { "servicios": [ … ] }.
lote.vacio422El lote no trae ningún servicio.
lote.demasiados413Más de 500 servicios en un lote.
lote.demasiadas_actualizaciones413El lote actualizaría más de 100 borradores ya existentes. Divídelo. Detalle en actualizaciones y maximo.
peticion.elemento422En un elemento del lote: no es un objeto con los datos del servicio.
lote.referencia_repetida409En un elemento del lote: la referencia ya aparece antes en el mismo lote.
lote.no_guardado503En un elemento del lote: no se pudo guardar. Reintenta el lote entero.
pdf.no_disponible503El PDF no se puede leer ahora mismo. Reintenta.
error_interno500Error nuestro. Reintenta con la misma referencia y cita el request_id.

Avisos (en avisos[], no bloquean)

Avisos que puede traer una respuesta correcta
CódigoQué significa
fecha.improbableFecha improbable: está a más de 30 días atrás o 60 adelante. Compruébala.
fecha.pasadaLa fecha del transporte ya ha pasado. Se puede emitir, y quedará la marca «emitido después de la fecha prevista».
peso.altoEl peso supera los 40.000 kg. Comprueba la cifra.
peso.ignoradoHas marcado «peso no determinable»: el peso en kilos se ignora.
campo.desconocidoUn campo del cuerpo no existe en el servicio: se ignora.
referencia.generadaSin referencia: se asigna «DD-{id}» y no hay idempotencia.
conductor.no_encontradoEl conductor indicado no es un conductor activo de la empresa: queda sin asignar.
conductor.pendienteEl conductor aún no está confirmado: queda sin asignar.
conductor.no_es_de_pruebasServicio de prueba con un conductor que no es «de pruebas»: queda sin asignar.
servicio.no_emitido_incompletoCon guardar_incompleto: true y datos que faltan, se guarda sin emitir.
lote.no_emiteEl lote ignora emitir: true; solo crea filas.
lote.no_guarda_incompletoEl lote ignora guardar_incompleto: true; un elemento con datos que faltan responde 422. Mándalo con POST /v1/servicios.
texto.dato_personalLas 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.

GET /conductores
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, conductor casa 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.
PUT /servicios/{id}/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.

POST /servicios/{id}/modificaciones
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.
409 · version.desfasada
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í.

POST /servicios/{id}/anular
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:

409 · servicio.difiere_del_emitido
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.

GET /servicios?modificado_desde=
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.
Bucle de sincronización en Python
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.

  1. 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.
  2. Cada servicio viaja con tu referencia, y ante un corte, un 5xx o un 429 reintentas la misma petición (respetando Retry-After).
  3. Un 422 enseña errores[] a quien puede corregir el dato, con el responsable según el art. 7.
  4. Un 409 servicio.difiere_del_emitido no se reintenta: se registra como modificación con un motivo real.
  5. 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.
  6. Los motivos y las observaciones no llevan teléfonos ni nombres de personas: son públicos para quien tenga el QR.
  7. El bucle de sincronización usa servidor_ahora menos 60 segundos y deduplica por id de modificación.
  8. Registras el X-Request-Id de cada respuesta con error.
  9. Has probado con dd_test_ un alta, una repetición, un error de datos, una modificación con version_base y una anulación.
  10. Los conductores están dados de alta y activos en el panel: la API no los crea.
  11. 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.

POST /servicios

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.json
Python
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.

Llamadas de la API v1
MétodoRutaQué hace
GET/yoEmpresa, entorno y prefijo de la clave.
POST/serviciosCrea y, por defecto, emite. Idempotente por referencia.
POST/servicios/loteHasta 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}/conductorAsigna a un conductor activo (telefono, email o conductor_id).
DELETE/servicios/{id}/conductorDeja el servicio sin conductor.
POST/servicios/{id}/modificacionescambios[], motivo y version_base. Nueva versión del PDF.
POST/servicios/{id}/emitirEmite un borrador.
POST/servicios/{id}/anularAnula con tipo y motivo; el PDF lleva la marca.
POST/servicios/{id}/finalizarFija el fin real del servicio.
GET/servicios/{id}/pdfPDF vigente o ?version=n, con la clave.
GET/servicios/{id}/qr.svg · qr.pngEl QR (solo codifica la URL pública), con la clave.
GET/conductoresConductores de la empresa, ?estado=.
GET/consumoDeCA 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.

LlamarWhatsApp