TuControlDeRutas

El documento de control ya tiene que ser electrónico. Circular sin él es infracción grave (LOTT 141.17)

Para desarrolladores · Desde el plan GESTIONA

API REST y Webhooks para tu ERP o TMS

Emite documentos de control (DeCA), cartas de porte y eCMR internacionales directamente desde tu sistema, y recibe avisos firmados en cada cambio de estado. Los tres tipos salen del mismo endpoint. Integración bidireccional con el patrón estándar REST + webhooks, sin tarifa por integración.

REST sobre HTTPSAutenticación BearerWebhooks firmados HMAC-SHA256Idempotente

Antes de integrar

¿Hay conector para mi ERP?

No hace falta ninguno: no se instala nada en tu sistema. La integración es por API REST sobre HTTPS con JSON, así que vale cualquier ERP, TMS o middleware que pueda hacer una llamada HTTPS, y los cambios de estado te llegan por webhooks firmados con HMAC-SHA256. Con la cuenta recibes el manual de integración en PDF y la colección de Postman con todas las llamadas. Para SAP hay además una guía complementaria, con el mapeo de campos habitual, un ejemplo en ABAP y dónde enganchar la emisión en el proceso, que enviamos a quien la pida.

Por la API salen los tres documentos de mercancías, cada uno con su página: el software DeCA para el documento de control, el software de carta de porte para la carta de porte nacional, que con los datos del art. 6 vale también como DeCA, y el eCMR, voluntario, para el transporte internacional. Si la carga es mercancía peligrosa, lo que tiene que llevar el documento está en carta de porte ADR. La hoja de ruta digital de viajeros no sale por esta API: se emite desde el panel.

¿Cuánto cuesta integrar?

Nada aparte: no hay tarifa de integración ni de alta, y no se paga por llamada ni por documento emitido por API. La API REST y los webhooks van incluidos en la cuota de los planes GESTIONA (desde 30 €/mes) y ESCALA; el plan de entrada, CUMPLE, no la incluye. Es una cuota cerrada por el tamaño de tu operativa, con todo dentro: el volumen de documentos al año solo sirve para dimensionarla. IVA no incluido.

Los tramos de cada plan y todo lo que incluye la cuota, en precios y planes.

¿Puedo probar sin enviar correos a nadie?

Sí, con una clave de pruebas. Se marca al crear la clave en Ajustes → API, y todo lo que entra por ella nace marcado como prueba. Por esos documentos no sale ningún correo: ni la copia con QR al conductor, al transportista o al cargador, ni los enlaces de firma AdES. Así puedes probar con conductores y pedidos reales sin escribir a nadie. Tampoco aparecen en la app del conductor. Lo que sí llega son los webhooks, para que pruebes tu receptor, y el PDF se genera igual, con marca de agua y sin validez legal. Esos documentos se pueden borrar cuando quieras. Para pasar a real, cambia la clave por una normal: los endpoints, los campos y las respuestas son los mismos.

1. Cómo empezar

  1. La API REST está disponible desde el plan GESTIONA. Con GESTIONA o ESCALA ya la tienes; la integración no lleva tarifa aparte.
  2. En el panel, ve a Ajustes → API y crea una clave con permiso de escritura.
  3. Guarda la clave (tcr_…): solo se muestra una vez.
  4. Empléala en la cabecera Authorization de cada petición.

Con la cuenta recibes el manual de integración completo en PDF y la colección de Postman con todas las llamadas de esta página listas para enviar. Esta página es el resumen; el manual es el documento de referencia.

2. Autenticación

Todas las llamadas requieren tu clave como Bearer token:

Authorization: Bearer tcr_TU_CLAVE

Base de la API: https://app.tucontrolderutas.com

Un 401 significa siempre que la clave no vale (ausente, mal copiada o revocada): no lo reintentes, revísala. Si el problema es nuestro recibirás un 503 con codigo: "servicio_no_disponible" y cabecera Retry-After, nunca un 401. Por eso es seguro que tu integración desactive la clave ante un 401.

Clave de pruebas

Al crear la clave puedes marcarla como de pruebas. El contrato no cambia en nada: mismos endpoints, mismos campos y mismas respuestas. Lo que cambia es que sus documentos salen con marca de agua y sin validez legal, no cuentan en tus estadísticas ni en tu volumen anual, y puedes borrarlos cuando quieras en vez de esperar el año de conservación que obliga la ley para los reales.

El día que salgas en real, cambia la clave por una normal en tu configuración y no toques nada más: ni una línea de código.

3. Crear un documento

POST

/api/v1/documentos

Crea un DeCA, una carta de porte o un eCMR según el campo modo. Los tres quedan emitidos al instante, con PDF y QR; la carta de porte y el eCMR mandan además los enlaces de firma AdES y se regeneran al firmar.

Campos obligatorios en los tres modos (datos del art. 6 · Orden FOM/2861/2012)

expedidor_nombreCargador contractual · razón social (art. 6.a)
expedidor_nifNIF/CIF del cargador (art. 6.a)
expedidor_direccionDomicilio del cargador (art. 6.a). La letra a) pide los tres: nombre, NIF y domicilio
transportista_nombreTransportista efectivo (art. 6.b)
transportista_nifNIF/CIF del transportista (art. 6.b)
origen_direccionDirección de origen (art. 6.c)
origen_ciudadCiudad de origen (art. 6.c)
destino_direccionDirección de destino (art. 6.c)
destino_ciudadCiudad de destino (art. 6.c)
descripcion_mercanciasNaturaleza de la mercancía (art. 6.d)
peso_valorPeso o magnitud de la mercancía (art. 6.d). Vale también el peso_kg de siempre: basta con uno de los dos
fecha_hora_previstaFecha del transporte, ISO 8601 (art. 6.f)
matricula_vehiculoMatrícula del vehículo (art. 6.g)

Obligatorios además, según el modo

modo_b

destinatario_nombre · destinatario_direccion

El destinatario (nombre y dirección) es mención obligatoria de la carta de porte (Ley 15/2009 art. 10 bis.1.e).

ecmr

pais_origen · pais_destino

Códigos de país de dos letras (ES, FR). El eCMR nace emitido y el PDF se regenera cuando firman.

Si falta alguno, la respuesta es 422 con la lista exacta en detalles[]. Nunca se emite un documento a medias.

Campos opcionales (los tres modos)

referencia_externaTu clave del documento (pedido, albarán o entrega). Clave de idempotencia (ver más abajo). Se imprime en los tres documentos con el rótulo «Ref. ERP:». Máx. 120 caracteres
referencia_clienteTu referencia del envío (pedido, albarán, expediente). Se imprime en los tres documentos: casilla 7 del DeCA, 8 de la carta de porte y 6 del eCMR; en el DeCA agrupado, la de cada entrega en su fila. Máx. 120 caracteres
peso_unidadUnidad de peso_valor: kg · t · L · m3 · bultos · pales · unidades, en mayúsculas o minúsculas (l o L). Por defecto kg. Fuera de esa lista, 422
peso_kgForma histórica del peso. Se sigue aceptando: si mandas peso_kg no hace falta peso_valor
conductor_idNuestro id del conductor (te lo devuelve POST /conductores). Es lo que hace que el documento aparezca en la app del conductor. Si no es de tu cuenta, 422
conductor_emailPor aquí le llega al conductor la copia con QR que debe llevar a bordo. Muy recomendado
conductor_nombre / conductor_dni_nieDatos del conductor. No son del art. 6 y no se imprimen en el PDF
transportista_emailBuzón del transportista efectivo. Útil al subcontratar. Si envías los dos, se notifica a los dos
email_expedidor / email_transportistaSolo en modo_b y ecmr: a cada dirección que venga le sale un correo real con su enlace de firma AdES (72 h). El documento se emite igual sin ellos; el enlace que falte se puede enviar después desde la ficha del documento en el panel
transportista_direccionDomicilio del porteador. Mención de la carta de porte (Ley 15/2009 art. 10.1.c) y del eCMR (CMR art. 6.1.c)
origen_cp / destino_cpCódigos postales
autorizacion_especialAutorización especial de circulación (art. 6.e), cuando el vehículo la necesita
num_bultosNúmero de bultos
remolque_matriculaMatrícula del remolque
tipo_vehiculo"rigido" (por defecto) o "articulado"
tipo_mercanciaCatálogo cerrado: general · temperatura_controlada · mercancia_peligrosa_adr · perecedera · animales_vivos. Otro valor, 422
numero_onu / clase_adrMercancía peligrosa (ADR 5.4.1.1.1 a y c): nº ONU (UN 1203) y los números de etiqueta de la tabla A (3 · 6.1 (3) · 2.1)
adr_designacion_oficialADR 5.4.1.1.1 b · designación oficial de transporte (nombre normalizado, no el comercial). En el eCMR, en español y en inglés, francés o alemán (5.4.1.4.1)
adr_grupo_embalajeADR 5.4.1.1.1 d · grupo de embalaje: I, II o III
adr_cantidad_totalADR 5.4.1.1.1 f · cantidad total de esa mercancía peligrosa
adr_codigo_tunelADR 5.4.1.1.1 k · código de restricción en túneles, en mayúsculas entre paréntesis. Ej: (D/E); sin código, (-)
mercanciasLista opcional: varias mercancías en el mismo documento, cada una con sus bultos, embalaje, marcas, peso, nº estadístico y, si es peligrosa, sus menciones ADR (una entrada por materia, ADR 5.4.1.1.1: UN, designación, etiquetas, grupo, túnel; cantidad; menciones adicionales; categoría; compartimentos de cisterna). Los tres PDF las imprimen una fila por línea, con totales, y el peso del documento (peso_valor) sale siempre: en el total si las líneas no lo traen o no suman, o debajo si no coincide con su suma
adr_indice_1136Índice de transporte ADR 1.1.3.6: por categoría {cantidad, valor} y total, tal como lo calcula el integrador (ADR 5.4.1.1.1 f, NOTA 1). Se imprime, no se recalcula
entregasSolo modo deca. Varias entregas en un DeCA agrupado (Resolución DGTCyF, apartado Sexto: el mismo cargador contractual y el mismo transportista efectivo en todas): una lista con todas, la 1 incluida. Arriba, una vez, lo del documento (cargador, transportista, origen, fecha, vehículo, conductor); en cada entrega, su destino, su destinatario, su mercancía con sus bultos y su peso, su referencia_cliente (el albarán) y, si carga en otro sitio, su lugar de carga (origen_direccion, origen_ciudad). Hasta 20 entregas. La respuesta trae el resumen de entregas. En modo_b o ecmr, 422 entregas_solo_deca
agencia_nombre / agencia_nif / agencia_direccionAgencia concertada: el porteador contractual cuando es distinto del transportista efectivo
documentos_anexosDocumentos entregados al transportista: casilla 5 del eCMR; en el DeCA y la carta de porte, en observaciones (p. ej. «Albarán nº 77121»)
destinatario_nombre / destinatario_direccionEn el DeCA es opcional (el art. 6 no lo pide), pero si la carga es ADR el 5.4.1.1.1 h) lo exige: sin él el documento NO vale como documento de transporte ADR y el propio PDF lo avisa en rojo. Razón social en destinatario_nombre y dirección completa (calle, número, CP y población) en destinatario_direccion. En modo_b es obligatorio
destinatario_nif / destinatario_cp / destinatario_ciudadOpcionales. El NIF/CIF del destinatario lo imprime el PDF del DeCA bajo la razón social y lo devuelve el GET (el ADR solo pide nombre y dirección). CP y población, para quien tenga la dirección en campos separados: el PDF del DeCA los imprime detrás de la dirección. En modo_b solo se guarda el NIF
destinatario_nombre = "Venta en Ruta"ADR 5.4.1.1.1 h): con destinatarios múltiples que no pueden identificarse al comienzo del transporte, las palabras “Venta en Ruta” pueden indicarse en lugar del nombre y la dirección. En España, solo para las materias del anejo 1, punto 6, del Real Decreto 97/2014 (UN 0333, 0334, 0335, 0336, 0337, 1001, 1002, 1006, 1013, 1046, 1049, 1066, 1072, 1073, 1202, 1951, 1954, 1956, 1963, 1965, 1972, 1977, 2187, 3156, 3157, 3158, 3161 y 3163). Se manda sin destinatario_nif ni destinatario_direccion. Si alguna materia peligrosa no está en esa lista, la mención no vale y hay que mandar nombre y dirección: la API guarda la mención tal cual, y el PDF y la página de inspección lo avisan en rojo en la casilla del destinatario
precio_referenciaPrecio o referencia al contrato. No es dato del DeCA: es mención de la carta de porte
observaciones / instrucciones_entregaTexto libre

Solo modo_b · menciones de la carta de porte (Ley 15/2009 art. 10)

cdp_naturaleza_mercanciasSi no la mandas, se usa descripcion_mercancias
cdp_num_bultosSi no lo mandas, se usa num_bultos
cdp_signos_identificacionSignos y señales de los bultos (art. 10.1.g)
cdp_clase_embalajeClase de embalaje (art. 10.1.j). No es el grupo de embalaje del ADR
cdp_quien_paga"cargador" o "destinatario" (art. 10.1.l). Otro valor, 422. Si no lo mandas queda sin declarar y el PDF omite la mención, en vez de afirmar quién paga
cdp_gastos_previsiblesGastos previsibles del transporte (art. 10.1.k)
cdp_declaracion_valorDeclaración de valor o interés especial en la entrega (art. 10.1.m). Número: fija el techo de responsabilidad del porteador
cdp_fecha_prevista_entregaFecha ISO 8601. Si no parsea, 422
cdp_instrucciones_adminInstrucciones administrativas (art. 10.1.n)
cdp_reservas_porteadorReservas del porteador (art. 11)
cdp_porteador_sucesivo_nombre / _nifPorteadores sucesivos (Ley 15/2009 art. 64; en el eCMR, casilla 17, CMR arts. 34-35): solo si varios porteadores ejecutan trayectos parciales del mismo transporte con esta misma carta de porte (relevo en ruta). No es la subcontratación: para eso están transportista_* (quien lo realiza) y agencia_* (quien lo contrató). También se admite en ecmr
cdp_mercancias_peligrosas / cdp_clase_peligroSi no los declaras, se deducen de numero_onu y clase_adr

Solo ecmr · Convenio CMR

destinatario_nombre / _direccion / _ciudad / _cp / _paisDestinatario, CMR art. 6.1.e. No devuelve 422 si falta, pero sin él el PDF imprime "(por designar)"
valor_mercanciasValor declarado. Número: fija el techo de responsabilidad
monedaPor defecto EUR
numero_cmrVuestro número de CMR, si lo lleváis
instrucciones_aduanaInstrucciones de aduana

Ejemplo

curl -X POST https://app.tucontrolderutas.com/api/v1/documentos \
  -H "Authorization: Bearer tcr_TU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "referencia_externa": "PEDIDO-2026-00123",
    "referencia_cliente": "ALB-77120",
    "expedidor_nombre": "Logística Ebro SL",
    "expedidor_nif": "B12345678",
    "expedidor_direccion": "Pol. Ind. Malpica, nave 7, 50057 Zaragoza",
    "transportista_nombre": "Trans López SL",
    "transportista_nif": "B87654321",
    "origen_direccion": "Pol. Ind. Malpica, nave 7",
    "origen_ciudad": "Zaragoza",
    "destino_direccion": "C/ Embajadores 120",
    "destino_ciudad": "Madrid",
    "descripcion_mercancias": "22 palés de bebidas",
    "peso_valor": 18400,
    "peso_unidad": "kg",
    "fecha_hora_prevista": "2026-10-06T08:15:00+02:00",
    "matricula_vehiculo": "4521KTM",
    "conductor_email": "conductor@translopez.com"
  }'

Respuesta 201 Created

{
  "ok": true,
  "idempotente": false,
  "documento": {
    "id": "8f0d…",
    "numero_control": "70268072",
    "estado": "emitido",
    "tipo_documento": "nacional",
    "modo": "solo_deca",
    "referencia_externa": "PEDIDO-2026-00123",
    "pdf_url": "https://<proyecto>.supabase.co/storage/v1/object/public/documentos/....pdf",
    "url_inspeccion": "https://app.tucontrolderutas.com/inspeccion/8f0d…",
    "url_pdf_publico": "https://app.tucontrolderutas.com/api/pdf/v/8f0d…",
    "created_at": "2026-10-06T06:15:02Z"
  }
}

# Si el documento se crea pero su PDF falla, la respuesta sigue siendo 201 y
# ademas trae "pdf_estado": "error" y "advertencia" con el motivo. Sin PDF no
# hay QR: ese documento AUN NO sirve ante inspeccion. No lo ignores.

Ojo con el campo modo: el que se ENVÍA y el que se DEVUELVE no usan el mismo vocabulario. En la petición es deca / modo_b / ecmr; en la respuesta y en los webhooks es solo_deca (DeCA) o carta_porte_como_deca, que vale tanto para la carta de porte nacional como para el eCMR. Para saber si es internacional mira tipo_documento (nacional o ecmr), no modo.

Tipos de documento (campo modo)

El mismo endpoint crea los tres tipos, y el catálogo es cerrado: deca, modo_b o ecmr. Cualquier otro valor devuelve 422 con codigo: "modo_no_reconocido". Una errata no emite nada: preferimos un 422 ruidoso a un documento equivocado viajando en la cabina. Si tu mapeo escribe el modo desde una tabla de configuración, revísalo antes de pasar a producción. El 422 llega también cuando la referencia_externa ya existe: la validación del cuerpo va por delante de la idempotencia, para que un reintento no tape el mapeo roto.

# Si omites "modo" se crea un DeCA. Es el unico respaldo que existe.

# modo_b · carta de porte que vale tambien como DeCA
{ "modo": "modo_b", ...campos comunes...,
  "destinatario_nombre": "Distribuciones Sur SL",
  "destinatario_direccion": "C/ Sevilla 10, Madrid",
  "email_expedidor": "firma@ebro.com",
  "email_transportista": "firma@translopez.com",
  "cdp_clase_embalaje": "Pales retractilados",
  "cdp_num_bultos": 24,
  "cdp_quien_paga": "destinatario",
  "cdp_declaracion_valor": 25000 }
# → queda "emitido" YA, con PDF y QR: la firma no es obligatoria para el
#   documento de control. En paralelo salen los enlaces de firma AdES, y al
#   firmar ambos el PDF se regenera en la MISMA URL (el QR no cambia).

# ecmr · carta de porte internacional
{ "modo": "ecmr", ...campos comunes...,
  "pais_origen": "ES", "pais_destino": "FR",
  "valor_mercancias": 18000, "moneda": "EUR",
  "email_expedidor": "firma@ebro.com",
  "email_transportista": "firma@translopez.com" }
# → queda "emitido" al instante, con PDF y QR, y salen los enlaces de firma AdES
#   (igual que modo_b). Hasta que firmen los dos, las casillas 22/23 del PDF dicen
#   "Pendiente de firma"; al firmar se regenera en la MISMA URL (documento.firmado).

# Cualquier otro valor escrito en "modo" → 422:
{ "ok": false, "error": "Validación de campos",
  "codigo": "modo_no_reconocido",
  "detalles": ["modo no reconocido: \"CDP\". Admitidos: deca, modo_b, ecmr ..."] }

Números: sin separador de millares

Number("1.500") en JavaScript es 1,5, no mil quinientos. Un ERP que serialice a la española mandaría un peso mil veces menor a un documento legal, y con coma decimal un dato que no es un número. No se adivina: se rechaza con 422 y el motivo exacto.

# MAL · 422 · separador de millares o coma decimal
{ "peso_valor": "1.500" }          se leeria como 1.5, no como 1500
{ "cdp_declaracion_valor": "12.000" }
{ "valor_mercancias": "1.234,56" } con coma decimal no es un numero

# BIEN · numero JSON, o cadena con punto decimal y sin separador de millares
{ "peso_valor": 1500 }
{ "peso_valor": "1500.50" }

# Campos con esta comprobacion:
#   peso_valor · peso_kg · num_bultos · cdp_num_bultos   (los tres modos)
#   cdp_declaracion_valor                                (solo modo_b)
#   valor_mercancias                                     (solo ecmr)
# En el modo que no guarda el campo NO se comprueba, porque ahi se descarta.

4. Idempotencia

Envía tu número de pedido en referencia_externa. Si repites la llamada con la misma referencia, la API no duplica el documento: devuelve el ya creado con 200 OK e idempotente: true. Así tu ERP puede reintentar tras un timeout sin riesgo de duplicar documentos. Se imprime en el PDF con el rótulo «Ref. ERP:». No la confundas con referencia_cliente: tu referencia del envío (el albarán), que también se imprime y no controla nada.

5. Restringir la clave a tus IP

Cada clave puede atarse a las direcciones de salida a internet desde las que llama tu sistema. Se configura en Ajustes → API → Claves, al crearla o después. Admite IPv4, IPv6 y notación CIDR.

203.0.113.7            una direccion suelta
198.51.100.0/24        un rango en notacion CIDR
2001:db8::/32          IPv6, igual de valido

Por qué importa: una clave de API acaba en un fichero de configuración, en un ticket y en el portapapeles de quien hace la integración. Con la lista puesta, una clave filtrada no sirve desde fuera de tu red. Si la dejas vacía no hay restricción, que es como funciona por defecto. Una petición desde una IP no declarada recibe un 403 con el código ip_no_permitida y la dirección que hemos visto, y queda registrada en la auditoría.

6. Consultar

GET

/api/v1/documentos?estado=&referencia_externa=&desde=&hasta=&pagina=&limite=

Lista paginada de documentos (50 por página por defecto, máx. 200). Una fecha mal formada devuelve 400 con el motivo.

GET

/api/v1/documentos/:id

Un documento con su lista fija de campos y sus firmas. Añade ?incluir_evidencia=1 para el trazo de la firma, la IP y el user-agent.

GET

/api/v1/documentos/ref/:referencia

El mismo objeto, por TU identificador (referencia_externa). No hace falta guardar nuestro UUID.

GET

/api/v1/conductores?limite=&pagina=

Conductores de la empresa. Sin parámetros viene la lista completa.

GET

/api/v1/vehiculos?limite=&pagina=

Vehículos de la empresa. Sin parámetros viene la lista completa.

La consulta de un documento devuelve una lista FIJA y publicada de campos, no "todo lo que haya en la tabla". Los tokens de firma no se devuelven nunca: son credenciales de un solo uso con las que se firma la entrega, y una clave de lectura no debe poder obtenerlas. Tampoco salen los campos de fontanería interna. Añadir campos a esa lista es aditivo; si alguna vez hubiera que retirar uno, se avisaría por adelantado.

Los dos endpoints de consulta de un documento devuelven exactamente el mismo objeto: la lista de campos vive en un solo sitio y no pueden divergir.

La URL del QR. Las tres consultas (la lista, por id y por referencia) devuelven url_pdf_publico, la URL que codifica el QR del PDF (descarga directa, sin login), y url_inspeccion, igual que la respuesta de crear y los webhooks. Si imprimes el QR en tus propios documentos, codifica url_pdf_publico tal cual, sin componer la URL.

Las firmas. Cada una trae quién firmó, cuándo, su tipo_firmante (receptor, el recibo de entrega, o conductor_testigo, una entrega rechazada con el motivo en notas), su numero_entrega y el hash_documento. En un DeCA agrupado la entrega 1 son las columnas destino_* y destinatario_* del documento, y la entrega k es paradas[k-2], con su referencia_cliente si se creó con entregas; las fotos de entrega (fotos_entrega) llevan el mismo numero_entrega. El hash_documento es el SHA-256 de los datos sellados en el instante de firmar: cargador (nombre, NIF y domicilio), transportista (nombre y NIF), origen y destino de la entrega 1, descripción de la mercancía, fecha prevista, matrícula, conductor, precio de referencia y número de control. Los datos de las entregas 2 en adelante no entran en él.

Las listas de conductores y vehículos traen total, pagina, limite y total_paginas. limite: null significa "sin paginar". Sin parámetros no se truncan solas a propósito: un ERP con 800 conductores viendo 50 y creyendo que tiene 50 es peor que no paginar. Un limite o una pagina que no sean enteros positivos devuelven 400 con el motivo, y pagina exige limite. Una lista vacía significa que no hay maestros: un fallo del servidor se ve como 500, no como cero filas.

7. Firma de la entrega

Un documento puede llevar tres firmas, y no se mezclan. La del contrato, en la carta de porte y el eCMR, la hacen el cargador y el transportista con firma electrónica avanzada: su enlace sale automáticamente al crear el documento, a los correos de email_expedidor y email_transportista. La de la entrega, en los tres documentos, es el acuse de recibo de quien recibe la mercancía: nombre, DNI y firma. Y la del conductor solo existe como testigo cuando quien recibe rechaza la entrega.

POST

/api/v1/documentos/:id/enlace-firma-entrega

El enlace de firma de la entrega, para que lo hagas llegar por tu canal: vale 48 horas y un solo uso. Con email en el cuerpo, además se lo mandamos por correo (como mucho 3 por hora por documento y entrega). Idempotente: mientras el enlace siga vivo devuelve el mismo (200, idempotente: true). Clave con permiso de escritura; el :id es nuestro UUID.

Por API nunca se firma en nombre de quien recibe: firma esa persona, en el móvil del conductor o con este enlace. El cuerpo es opcional: entrega solo hace falta en un DeCA agrupado (por API cada documento tiene una sola entrega) y email si quieres que enviemos el correo. La respuesta trae el enlace (url, caduca_at) y si el correo salió; lleva Cache-Control: no-store, y en la auditoría queda documento.enlace_firma con la huella del enlace, nunca el enlace. Con una clave de pruebas se devuelve el enlace y se puede firmar con él, pero no sale ningún correo.

Pedir el enlace no dispara ningún webhook. Lo que se firme te llega por los de siempre (documento.firmado_receptor y documento.completado, o documento.entrega_rechazada) y en las firmas del documento, con su numero_entrega.

Finaliza después del recibo, no antes. Un documento finalizado no admite la firma de la entrega (409 con estado_no_admite_firma_entrega) y deja de verse en la app del conductor. La firma de la entrega no es obligatoria: si no la vas a pedir, finaliza cuando termine el servicio.

8. Fin de servicio, borrado y auditoría

Marcar el fin del servicio es lo que arranca el calendario legal del documento: los 16 días de descarga pública, el año de conservación mínima y los 4 años hasta la destrucción definitiva. Si nadie lo marca, ese reloj no empieza a contar.

POST

/api/v1/documentos/:id/finalizar

Marca el fin del servicio y devuelve calculadas las tres fechas que se derivan. Idempotente: repetirlo no mueve la fecha y devuelve el mismo cuerpo completo.

DELETE

/api/v1/documentos/:id

Borrado a petición (RGPD art. 17). Devuelve 409 con un código estable y la fecha exacta si aún no ha pasado el año de conservación obligatoria.

POST

/api/v1/conductores/:id/anonimizar

Anonimización irreversible de un conductor que deja la empresa (lo mismo que Eliminar en el panel). Los documentos ya emitidos no cambian: conservan su propia copia de los datos por obligación legal (RGPD art. 17.3.b).

GET

/api/v1/auditoria

Registro de auditoría en NDJSON incremental, para tu SIEM. Filtros por acción, tipo de actor, resultado, recurso y fechas.

Estados desde los que se puede finalizar: emitido, firmado_*, completado, entrega_rechazada y pendiente_firma_aes (de los firmado_*, el único que produce un flujo es firmado_receptor, el paso previo a completado; firmado_cargador y firmado_conductor están en el catálogo, ningún flujo los produce y se tratan como emitido). Un documento que siga en ese estado (sin PDF ni QR, con las firmas AdES sin llegar) también se puede cerrar: se le anota el fin de servicio pero conserva su estado, porque nunca se emitió. La respuesta lo dice con emitido: false y una nota. No compruebes el éxito con estado === "finalizado"; compruébalo con ok: true. En cualquier otro estado se devuelve 409 con codigo: "estado_no_finalizable", estado_actual y la lista de estados admitidos.

Un documento de control no se puede borrar antes de 1 año desde el fin del servicio: lo impone la Resolución DGTCyF (apartado Segundo.4) y no lo puede saltar nadie. A partir de ahí sí se borra a petición, y a los 4 años se destruye solo. La única excepción son los documentos que nunca llegaron a emitirse (borrador o pendiente_firma_aes): no fueron válidos ni un segundo, no tienen PDF ni QR y no hay nada que conservar, así que se borran en el acto, con o sin fin de servicio marcado.

En la exportación al SIEM aparecen también los accesos de nuestro soporte a tu cuenta (superadmin.acceder), los intentos de acceso denegados (usuario.login con resultado: denegado) y el uso de una clave que ya habías revocado (api.autenticar con detalles.motivo: "clave_revocada", que es la señal de una credencial filtrada). La tabla es de solo-añadir: nadie puede reescribirla, nosotros tampoco.

9. Sincronizar maestros

Da de alta y mantén al día conductores, vehículos y clientes desde tu ERP. El alta es idempotente por la clave natural (dni_nie / matricula / nif): si ya existen, se devuelven sin duplicar (200). Para cambiar un dato, PATCH: manda solo lo que cambia y localiza la ficha por tu propia clave, sin guardar identificadores nuestros.

POST

/api/v1/conductores

Alta de conductor. Obligatorios: nombre, apellidos y dni_nie.

PATCH

/api/v1/conductores/:id

Modifica un conductor (nombre, apellidos, telefono, email, activo). El :id puede ser nuestro uuid o su dni_nie. Lo que no mandas no se toca. Cambiar el email de quien entra en la app reinicia su acceso y le envía al correo nuevo la invitación a la app; activo: false cierra su acceso.

POST

/api/v1/vehiculos

Alta de vehículo. Obligatoria: matricula. tipo: rigido (por defecto) / articulado / furgon / semirremolque.

PATCH

/api/v1/vehiculos/:id

Modifica un vehículo (tipo, remolque, activo). El :id puede ser nuestro uuid o su matricula; si la matrícula tiene varias fichas activas (una por semirremolque), el uuid.

POST

/api/v1/clientes

Alta de cliente (contraparte). Obligatorio: nombre. Idempotente por nif, y por nombre exacto si no hay nif.

PATCH

/api/v1/clientes/:id

Modifica un cliente (nombre, direccion, ciudad, cp, pais, email, telefono, notas, activo). El :id puede ser nuestro uuid o su nif.

GET

/api/v1/clientes?limite=&pagina=

Clientes de la empresa. Sin parámetros viene la lista completa.

dni_nieEl DNI, el NIE o el documento de identidad del país del conductor, pasaporte incluido. La letra de control solo se comprueba cuando el valor TIENE forma española: un documento extranjero se manda tal cual y no se valida
telefonoFormato español (612345678, +34612345678) o cualquier país con prefijo internacional explícito (+351912345678, 00351912345678). Sin prefijo se sigue interpretando como español
emailOpcional, pero si lo mandas debe ser una dirección válida: es la vía por la que le llega al conductor la copia con QR que exige el Séptimo de la Resolución. Si no lo tienes, déjalo vacío
matriculaCualquier formato, también extranjeras. No se valida por forma: el art. 6.g pide "la matrícula", no una matrícula española
remolqueMatrícula del semirremolque, si lo lleva. Una tractora puede tener varias fichas, una por semirremolque (se dan de alta en el panel o por CSV). El alta por API es idempotente por matricula: no escribe nada y devuelve la ficha de ese semirremolque si existe, o la más reciente
tiporigido · articulado · furgon · semirremolque (un remolque suelto de la flota: su matricula es la del semirremolque). No distingue mayúsculas ni tildes. Fuera del catálogo, 422: antes un ERP que mandara su catálogo en mayúsculas registraba TODA su flota como rígida creyendo que estaba bien
# Alta de conductor (idempotente por dni_nie)
curl -X POST https://app.tucontrolderutas.com/api/v1/conductores \
  -H "Authorization: Bearer tcr_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{ "nombre": "Juan", "apellidos": "Martín Ruiz",
        "dni_nie": "51234829Y", "telefono": "612345678",
        "email": "juan@translopez.com" }'
# → { "ok": true, "idempotente": false, "conductor": { "id": "…", … } }
#   Ese "id" es el conductor_id que se manda al crear el documento.

# Conductor extranjero: se acepta su documento tal cual y su telefono con prefijo
  -d '{ "nombre": "Ion", "apellidos": "Popescu",
        "dni_nie": "RO-CJ-472019", "telefono": "0040712345678" }'

# Alta de vehiculo (idempotente por matricula)
curl -X POST https://app.tucontrolderutas.com/api/v1/vehiculos \
  -H "Authorization: Bearer tcr_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{ "matricula": "4521KTM", "tipo": "articulado", "remolque": "R5678CD" }'

# Alta de cliente / contraparte (idempotente por nif)
curl -X POST https://app.tucontrolderutas.com/api/v1/clientes \
  -H "Authorization: Bearer tcr_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{ "nombre": "Logistica Ebro SL", "nif": "B50123456",
        "direccion": "Pol. Ind. Malpica, calle A, 12",
        "ciudad": "Zaragoza", "cp": "50016", "pais": "ES" }'

# MODIFICAR: manda solo lo que cambia, localizando la ficha por TU clave natural
curl -X PATCH https://app.tucontrolderutas.com/api/v1/conductores/51234829Y \
  -H "Authorization: Bearer tcr_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{ "telefono": "+34600111222" }'
# → { "ok": true, "actualizado": true, "campos_actualizados": ["telefono"], … }
#   Lo que no mandas NO se toca. "email": null lo vacia a proposito.
#   La clave natural (dni_nie / matricula / nif) no se cambia por esta via.

# Baja de una ficha: deja de aparecer para elegir y no toca los documentos emitidos
curl -X PATCH https://app.tucontrolderutas.com/api/v1/vehiculos/4521KTM \
  -H "Authorization: Bearer tcr_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{ "activo": false }'

10. Webhooks

Registra una URL en Ajustes → API → Webhooks y te enviaremos un POST cada vez que un documento cambie de estado, sin que tengas que sondear la API. Puedes suscribirte a * (todos) o a los eventos concretos de la lista. Un evento que no esté en ella se rechaza con 400 y la lista de los válidos.

Eventos

documento.emitidoDocumento creado, con PDF y QR: válido ante inspección. Al crear, en los tres modos
documento.firmadoLas dos firmas AdES completas: el documento gana su fuerza probatoria como contrato
documento.firmado_receptorTodas las entregas tienen su recibo firmado (art. 12 de la Ley 15/2009). Sale al cerrarse el documento, seguido de documento.completado; en un DeCA agrupado, con la última entrega
documento.completadoEntrega terminada: con el último recibo firmado, o cuando la oficina la da por terminada desde el panel
documento.entrega_rechazadaSe rechaza una entrega. Llega en el momento, también en un DeCA agrupado con entregas por resolver (el cuerpo trae entonces estado: emitido)
documento.finalizadoFin del servicio: arranca el calendario legal (descarga pública, conservación y purga)
documento.eliminadoDocumento borrado a petición (RGPD art. 17), por API o desde el panel

Esos son todos. Al completarse las dos firmas AdES (carta de porte y eCMR) llega documento.firmado y el PDF se regenera en la misma URL. Como cada entrega puede reintentarse, el receptor debe ser idempotente.

DeCA agrupado (varias entregas en un documento, apartado Sexto de la Resolución). Se crea por API con entregas (modo deca), o lo emiten tu oficina o tus conductores, y te llega por los mismos webhooks. Cada entrega tiene su recibo y el documento sigue en emitido hasta resolver la última: un recibo intermedio no manda nada, cada rechazo llega en el momento y documento.firmado_receptor y documento.completado llegan con la última entrega, si todas tienen recibo. Si alguna se rechazó, el documento se cierra en entrega_rechazada y documento.entrega_rechazada puede llegarte más de una vez: distínguelo por el estado del cuerpo. El cuerpo no dice qué entrega fue; lo dicen las firmas del documento, por su numero_entrega.

Qué recibes

POST  https://tu-erp.com/webhooks/tcr
X-TCR-Evento:     documento.completado
X-TCR-Entrega:    3f9c1d0e-…          ← id de ESTA entrega · usalo para deduplicar
X-TCR-Timestamp:  1791450730           ← segundos Unix
X-TCR-Firma:      sha256=2b1c…         ← HMAC-SHA256 del cuerpo
X-TCR-Firma-V2:   sha256=9ad4…         ← HMAC-SHA256 de "<timestamp>.<cuerpo>"
User-Agent:       TuControlDeRutas-Webhook/1

{
  "evento": "documento.completado",
  "enviado_at": "2026-10-06T14:32:10.412Z",
  "prueba": false,
  "documento": {
    "id": "8f0d…",
    "numero_control": "70268072",
    "estado": "completado",
    "tipo_documento": "nacional",
    "modo": "solo_deca",
    "referencia_externa": "PEDIDO-2026-00123",
    "pdf_url": "https://<proyecto>.supabase.co/storage/v1/object/public/documentos/….pdf",
    "url_inspeccion": "https://app.tucontrolderutas.com/inspeccion/8f0d…",
    "url_pdf_publico": "https://app.tucontrolderutas.com/api/pdf/v/8f0d…",
    "origen_ciudad": "Zaragoza",
    "destino_ciudad": "Madrid",
    "created_at": "2026-10-06T06:15:02.000Z",
    "updated_at": "2026-10-06T14:32:09.880Z"
  }
}

El objeto documento lleva siempre las mismas claves; lo que no se conozca viaja como null. El campo prueba también va siempre: false en los eventos reales y true en el que lanzas desde el panel. El evento de prueba tiene exactamente la misma forma que el real (mismas claves y en el mismo orden), así que puedes cablear y validar tu esquema contra él: lo que recibas en producción es idéntico salvo los valores.

Entrega y reintentos

Tu endpoint tiene 1,5 segundos para responder 2xx. Si tarda más, abortamos la conexión y la entrega se reintenta: 6 intentos con esperas de 1, 5, 30, 120 y 360 minutos, unas 8 horas y media de margen. El barrido corre cada 5 minutos, así que si tu receptor se cae diez minutos el evento vuelve enseguida, no al día siguiente. Encola y responde de inmediato; no hagas el trabajo pesado dentro de la petición del webhook, y haz tu receptor idempotente: un mismo evento puede llegarte más de una vez. Deduplica por X-TCR-Entrega.

El secreto de firma

Cada webhook lleva su propio secreto. Puedes traer el tuyo al crearlo, útil si ya lo guardas en tu gestor de secretos, así no tiene que salir de ahí, o dejar el campo vacío y lo generamos nosotros. Mínimo 24 caracteres, máximo 200, ASCII imprimible y sin espacios.

Se rota sin perder nada: hay un botón para ello y no hace falta borrar el webhook ni su historial. Desde la rotación firmamos con el nuevo, y como reintentamos hasta 6 veces durante unas 8 horas, tienes ese margen para actualizarlo sin perder ningún evento.

Verificar la firma

Cada envío incluye dos firmas, y las dos van siempre. X-TCR-Firma es el HMAC-SHA256 del cuerpo crudo con tu secret. X-TCR-Firma-V2 firma "<timestamp>.<cuerpo>", y es la que deberías usar: comprobando que X-TCR-Timestamp cae dentro de una ventana (5 minutos es lo habitual) puedes rechazar una entrega vieja reenviada, cosa que la v1 por sí sola no permite. La v1 se mantiene para no romper a quien ya la verifica.

// Node.js (Express) · se verifica la V2, que ademas corta los reenvios
const crypto = require('crypto')

app.post('/webhooks/tcr', express.raw({ type: '*/*' }), (req, res) => {
  const ts    = req.headers['x-tcr-timestamp'] || ''
  const firma = req.headers['x-tcr-firma-v2'] || ''

  // Ventana anti-reenvio: sin ella, una entrega capturada vale para siempre.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).end()

  const esperado = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(ts + '.' + req.body)   // cuerpo CRUDO, sin parsear
    .digest('hex')

  const a = Buffer.from(String(firma)), b = Buffer.from(esperado)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end()

  // Responde YA (tienes 1,5 s) y procesa despues, de forma idempotente:
  // una misma entrega puede llegarte mas de una vez. Deduplica por X-TCR-Entrega.
  res.status(200).end()
  encolar(JSON.parse(req.body))
})
<?php // PHP
$cuerpo = file_get_contents('php://input');
$ts     = $_SERVER['HTTP_X_TCR_TIMESTAMP'] ?? '';
$firma  = $_SERVER['HTTP_X_TCR_FIRMA_V2']  ?? '';

if (abs(time() - (int)$ts) > 300) { http_response_code(401); exit; }

$esperado = 'sha256=' . hash_hmac('sha256', $ts . '.' . $cuerpo, $WEBHOOK_SECRET);
if (!hash_equals($esperado, $firma)) { http_response_code(401); exit; }

http_response_code(200);          // responde primero
$evento = json_decode($cuerpo, true);
// … encola y procesa fuera de esta peticion …

11. Errores y límites

Toda respuesta de error lleva ok: false y error (texto para leer). Cuando existe, trata el campo codigo, que es estable, y no el texto del mensaje, que puede cambiar. No todos los errores traen codigo; los de validación de documentos y de maestros traen siempre detalles[], un array de cadenas campo a campo.

200Idempotente: ya existía ese documento, conductor o vehículo. No se ha duplicado nada
207Solo en /documentos/bulk: el lote se procesó con errores parciales. Mira resultados[] documento a documento
400Cuerpo JSON no válido, o un parámetro de consulta mal formado (fecha, limite, pagina, accion). Se dice cuál y por qué
401API key ausente, mal formada, inexistente o revocada. NO reintentes: revisa la clave
403La cuenta no tiene API en su plan (desde GESTIONA), la clave es de solo lectura o la petición viene de una IP no declarada (codigo ip_no_permitida)
404El documento, la referencia o el conductor no existen en tu cuenta
409La operación choca con el estado o con un plazo legal: codigo estado_no_finalizable, servicio_sin_finalizar o conservacion_obligatoria. En el enlace de firma de la entrega, estado_no_admite_firma_entrega o entrega_ya_resuelta
422Validación: faltan campos obligatorios o un valor está fuera de catálogo. En la creación de documentos y en los maestros, con detalles[]; en el enlace de firma de la entrega, con su codigo
429Límite por clave superado. Siempre con cabecera Retry-After
500Fallo nuestro procesando la petición. Reintenta con backoff
503No hemos podido comprobar la credencial (incidencia nuestra, no tuya). Con Retry-After y codigo servicio_no_disponible. Tu clave NO ha sido rechazada. En el enlace de firma de la entrega, también firma_por_entrega_no_disponible, con Retry-After

Límites por clave

POST /documentos600 creaciones por minuto · Retry-After: 30
POST /documentos/bulk60 peticiones de lote por minuto, hasta 500 documentos cada una · Retry-After: 30
POST y PATCH de /conductores, /vehiculos y /clientes120 por minuto, un solo contador para los seis (altas y modificaciones de los tres maestros) · Retry-After: 60
POST /documentos/:id/enlace-firma-entrega120 por minuto, con su propio contador · Retry-After: 60. Aparte, como mucho 3 correos por hora por documento y entrega: pasado ese tope el enlace se devuelve igual, sin correo y sin 429

Son cortafuegos anti-abuso, no un freno a la emisión legítima. Y como crear es idempotente por referencia_externa, reintentar tras un 429 nunca duplica un documento. Respeta el Retry-After: la ventana es fija, así que un reintento anticipado se lleva otro 429.

Códigos estables

modo_no_reconocido422 · el valor de "modo" no está en el catálogo (deca, modo_b, ecmr)
dni_nie_no_valido422 · tiene forma de DNI/NIE español y la letra de control no cuadra
telefono_no_valido422 · el teléfono no se reconoce ni como español ni con prefijo internacional
email_no_valido422 · el email del conductor, o el del enlace de firma de la entrega, no tiene formato de dirección
tipo_no_reconocido422 · el tipo de vehículo no está en el catálogo
matricula_ambigua409 · PATCH de vehículo por matrícula cuando esa matrícula tiene varias fichas activas (una por semirremolque). Trae las fichas en vehiculos: se modifica cada una por su id
combinacion_en_uso409 · el remolque (o el activo: true) pedido convertiría el vehículo en otra combinación de matrícula y semirremolque que ya está activa. No se cambia nada
campos_obligatorios422 · falta un campo obligatorio del alta de maestros
limite_plan_vehiculos422 · reservado: hoy ningún plan limita vehículos, así que no se devuelve; solo saltaría si se reintrodujera un tope
estado_no_finalizable409 · el documento no está en un estado desde el que se pueda finalizar
servicio_sin_finalizar409 · no se puede borrar: el plazo no ha empezado porque nadie marcó el fin de servicio
conservacion_obligatoria409 · no se puede borrar todavía. Trae disponible_desde con la fecha exacta
fecha_fin_invalida500 · la fecha de fin guardada está corrupta. No es reintentable: avísanos
estado_no_admite_firma_entrega409 · el estado del documento no admite la firma de la entrega (solo emitido, firmado_cargador y firmado_conductor). Trae estado_actual y estados_validos
entrega_ya_resuelta409 · esa entrega ya tiene su recibo o su rechazo: no hace falta otro enlace. Trae entrega y estado_entrega
entrega_no_valida422 · entrega no es un número entero a partir de 1
entrega_requerida422 · el documento es un DeCA agrupado y falta decir la entrega. Trae total_entregas
entrega_no_existe422 · la entrega pedida pasa del total del documento. Trae total_entregas
cuerpo_no_valido400 · el cuerpo del enlace de firma de la entrega no es JSON o no es un objeto
limite429 · más de 120 peticiones de enlaces de firma por minuto con la misma clave. Con Retry-After
error_consulta / error_enlace500 · no hemos podido leer el documento o preparar el enlace de firma. Reintenta
firma_por_entrega_no_disponible503 · la firma por entrega no está disponible en este momento. Con Retry-After
ip_no_permitida403 · la clave está restringida y la petición no viene de una IP declarada
servicio_no_disponible503 · no hemos podido comprobar la credencial. Reintenta

¿List@ para integrar?

Solicita tu cuenta desde el plan GESTIONA y genera tu primera clave en minutos. Sin tarifa por integración.

Sigue leyendo