Índice de la API
Cómo funciona
Una API REST con JSON de ida y vuelta, toda en inglés: rutas, campos, valores y errores. Con tres llamadas tienes el porte documentado: creas el envío y emites (POST /shipments), consultas cómo va y si el albarán está firmado (GET /shipments/:id) y descargas el PDF con su QR (GET /documents/:id/pdf).

GET /documents/:id/pdf: el mismo PDF que sale en pantalla, con su QR. Este es el de la prueba, con datos de ejemplo.Para enterarte de lo que pasa sin preguntar, los webhooks: te avisamos cuando un documento se emite, cambia, se anula o se firma.
Antes de empezar: qué datos lleva el DeCA, varios envíos en un DeCA y, si usas Odoo, los módulos ya hechos.
¿Usas a3ERP, Holded, Odoo u otro? Si prefieres que lo conectemos nosotros, escríbenos: es lo que hacemos en Francodesystems.
Idempotente por reference
reference, no se crea otro: se devuelve el que hay, con "duplicate": true. Reintentar una llamada que se cortó no duplica nada.Primer mes
"isDemo": false.# 1. Crear el envío y emitir el DeCA y el albarán
curl -X POST https://decafirma.com/api/v1/shipments -H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" -d '{ …, "issue": "both" }'
# 2. Cómo va (y si el albarán está firmado)
curl https://decafirma.com/api/v1/shipments/cmu… -H "Authorization: Bearer dfk_…"
# 3. El PDF, con su QR
curl https://decafirma.com/api/v1/documents/cmv…/pdf -H "Authorization: Bearer dfk_…" -o DECA.pdfLa clave
Quien administra la empresa la crea en Configuración Conectar tu programa (API). Se enseña una sola vez. Va en cada petición, en la cabecera Authorization.
- Lo que se emite con una clave va a nombre de quien la creó. Si esa persona deja de poder emitir, la clave deja de valer (
key_owner_forbidden). - Como mucho 120 peticiones por minuto por clave (
rate_limited). - En el plan gratis no hay API (
plan_not_allowed).
Para que en el historial del documento salga quién lo hizo en tu programa («María Pérez (desde Odoo)»), manda también estas dos cabeceras, las dos opcionales:
- X-DecaFirma-User
- El nombre de la persona en tu programa.
- X-DecaFirma-Client
- El nombre de tu programa.
Authorization: Bearer dfk_… X-DecaFirma-User: María Pérez X-DecaFirma-Client: Odoo
{
"error": {
"code": "missing_api_key",
"message": "Missing API key: send the header «Authorization: Bearer dfk_…»."
}
}Errores
Siempre en JSON y en inglés, con un code estable para tu programa y un message que se entiende, para enseñarlo o guardarlo en tu registro. details solo viene cuando hay más que decir (en validation_error, la lista de mensajes).
En las llamadas que emiten o cambian un envío (/shipments, /shipments/:id/issue, /shipments/:id, /shipments/:id/vehicle), un 422 trae el envío entero y, además, "error": { code, message, details }: lo guardado no se pierde.
{
"error": {
"code": "validation_error",
"message": "The request is not valid.",
"details": ["transportDate must be YYYY-MM-DD."]
}
}- missing_api_key401401Falta la cabecera Authorization.
- invalid_api_key401401La clave no existe o está revocada.
- rate_limited429429Más de 120 peticiones en un minuto con la misma clave.
- plan_not_allowed403403El plan de la empresa no incluye la API (en el plan gratis no hay).
- addon_required403403Falta el extra de conexión con tu programa en la cuenta.
- key_owner_forbidden403403Quien creó la clave ya no puede emitir en la empresa. Crea otra desde Configuración.
- not_found404404El envío, documento, ruta o papel no existe o no es de tu empresa.
- validation_error400400La petición no es válida.
detailstrae la lista de mensajes, campo a campo (también un campo que no conocemos). - invalid_series400400El
seriesIdno es una línea de numeración de albaranes viva de tu empresa. - payload_too_large413413El cuerpo pasa del límite: 5 MB por PDF y 40 MB entre todos los de una llamada; en
/sync, 500 elementos. - quota_exceeded429429Sin lecturas disponibles en la cuenta (
/parse). - service_unavailable503503El servicio no está disponible ahora mismo. Reintenta más tarde.
- upstream_error502502Ha fallado un servicio del que dependemos. Reintenta en un momento.
- not_a_shipment422422El texto de
/parseno encarga ningún porte. - extraction_failed422422No se ha podido sacar nada del texto de
/parse. - invalid_webhook_url400400La dirección del webhook no es una URL https válida.
- email_failed4xx4xxNo se ha podido mandar el correo de
/documents/:id/send. El motivo, endetails.reason. - unprocessable422422Una regla del negocio lo impide: falta un dato, un NIF mal, un albarán ya firmado, una fecha fuera de plazo… (al emitir, corregir o en una ruta). El motivo, en
details.reason: ver Motivos de un 422.
Motivos de un 422
Cuando una regla del negocio impide emitir, corregir, anular o hacer una ruta, la respuesta es 422 con code: "unprocessable", un message en inglés y details con el motivo en details.reason y sus datos al lado. Los reason son una lista cerrada (la de abajo): programa contra ellos y arma tu propio texto, por ejemplo en castellano, con los datos.
Los nombres de campo de details (fields, field) son los del cuerpo de POST /shipments. Si hay más de un problema, arriba va el primero y details.problems los trae todos, cada uno con su reason, sus datos y su message. En POST /shipments, details.document dice qué documento no salió (deca o delivery_note).
{
"id": "cmu…",
"status": "draft",
"documents": [],
"error": {
"code": "unprocessable",
"message": "The DeCA was not issued: Missing required fields: plate.",
"details": {
"reason": "missing_fields",
"fields": ["plate"],
"document": "deca"
}
}
}{
"error": {
"code": "unprocessable",
"message": "shipperTaxId (B4300000) is incomplete: it is missing digits. carrierTaxId (B43000001) is not a valid Spanish tax ID: check the digits and the letter.",
"details": {
"reason": "invalid_tax_id",
"field": "shipperTaxId",
"value": "B4300000",
"problem": "incomplete",
"problems": [
{ "reason": "invalid_tax_id", "field": "shipperTaxId", "value": "B4300000", "problem": "incomplete", "message": "…" },
{ "reason": "invalid_tax_id", "field": "carrierTaxId", "value": "B43000001", "problem": "invalid", "message": "…" }
]
}
}
}- missing_fieldsfields: string[], stop?, position?Faltan datos para emitir.
fieldscon los nombres del cuerpo de/shipments(plate,shipperTaxId…). En una ruta,stopes la parada (1 la primera de las que mandas; sin él, falta en todas: la parte común) oposition, el sitio del envío en la ruta. - invalid_tax_idfield, value, problem: incomplete | invalid, position?Un NIF que no cuadra:
fieldesshipperTaxIdocarrierTaxId. - transport_date_expiredfield, value (YYYY-MM-DD), daysLa fecha del transporte es de hace más de
daysdías: el documento nacería con la descarga cerrada. - missing_delivery_note_numbersin datosNi número de albarán ni línea de numeración (también en
skipped, sin su número ennumbers). - invalid_delivery_note_numbernumberEl número del albarán lleva caracteres que no valen o pasa de 40.
- duplicate_delivery_note_numbernumberYa tienes un documento con ese número: un número de albarán no se repite.
- document_already_existsdocument: deca | delivery_note, reference, documentIdEl envío (o la ruta) ya tiene ese documento vivo: modifícalo o anúlalo.
- document_voidedreferenceEl documento está anulado: no se modifica.
- document_already_voidedreferenceYa estaba anulado.
- delivery_note_signedreferenceEl albarán ya está firmado: no se reescribe ni se quita de la ruta. Se anula y se emite otro.
- reason_requiredsin datosFalta el motivo del cambio o de la anulación.
- shipment_in_routerouteIdEl envío va en una ruta: su DeCA es el de la ruta.
- shipment_not_in_routeshipmentIdEse envío no va en esa ruta.
- shipments_have_own_decareferences: string[]Hay envíos con su propio DeCA: anúlalos antes de sacar el de la ruta.
- route_too_few_stopsminimumUna ruta necesita al menos dos paradas (o una, al añadir).
- route_field_mismatchposition, fieldUn envío de la ruta no tiene el mismo
shipperTaxId,carrierTaxId,transportDateoplateque el primero (apartado sexto). - route_weight_mismatchstop, deliveryStop, weightKg, deliveryWeightKgUna recogida y una entrega posterior del mismo sitio y con los mismos bultos llevan unos kilos que solo se diferencian en ceros (53 y 5.300): pon los mismos en las dos.
- route_goods_onlypositionLas rutas son de mercancías.
- route_without_decasin datosLa ruta no tiene un DeCA vivo.
- deca_not_convertibledocumentIdEl DeCA de
fromDocumentIdno se puede convertir en ruta (anulado o ya es una ruta). - deca_already_in_routedocumentId, routeIdEl envío del DeCA de
fromDocumentIdya va en una ruta. - shipment_not_foundsin datosEl envío no existe.
- document_not_foundsin datosEl documento no existe.
- route_not_foundsin datosLa ruta no existe.
- no_subscriptionsin datosLa empresa no tiene suscripción.
- demo_endeddemoEndedAtLa prueba ha terminado: activa la cuenta.
- account_inactivesin datosLa cuenta no está activa: no se emiten documentos nuevos.
- free_plan_not_allowedfeatureEso no va en el plan gratis.
- free_plan_limit_reacheddocument, limitHechos los documentos gratis del mes.
- fleet_limit_exceededvehicle, plate | carrierTaxId, maxVehicles, graceDaysUn vehículo de más para el plan, pasados los días de margen.
- field_not_allowedfields: string[], documentEsos campos no se pueden cambiar con este permiso.
- not_yourssin datosEl porte o la ruta tiene que ir a nombre de quien lo hace.
- permission_denieddocumentSin permiso para ese documento.
- email_recipients_limitlimitTope de destinatarios distintos en 24 horas (
email_failed, 429). - email_same_recipient_limitemail, limitTope de correos al mismo destinatario hoy (
email_failed, 429). - invalid_emailemail | field, valueNo es una sola dirección de correo válida (varias juntas, con nombre,
mailto:…). Encompany.skippedde/sync, el correo de la empresa que no se ha guardado. - invalid_platefield, valueEn
skippedde /sync: la matrícula no parece una matrícula. - invalid_formatfield, value, textEn
skippedde/sync: el formato de numeración no vale (text, el porqué, en castellano). - format_takenformat, seriesEn
skippedde/sync: otra serie ya usa ese formato. - duplicate_external_idsin datosEn
skippedde/sync: el mismoexternalIddos veces en la llamada; vale el primero. - unknown_referencefield, valueEn
skippedde/sync:driverExternalIdoemployerExternalIdno es nadie (vivo) de la llamada ni de DecaFirma; el deemployerExternalIdtiene que ser uncarrier. - plate_takenplateEn
skippedde/sync: la matrícula ya es de otro vehículo con otroexternalId. - tax_id_takentaxId, takenBy?En
skippedde/sync: el NIF ya es de otra persona viva del equipo (una persona, una ficha);takenBy, suexternalId. - tax_id_lockedvalue, currentEn
company.skippedde/sync: el NIF de la empresa no se cambia (va impreso en todo lo emitido). Si no es el tuyo, la clave es de otra empresa. - invite_failedtextEn
skippedde/sync: la persona se guardó, pero la invitación no salió (sin correo, sin DNI, ese correo ya está dentro…). - business_ruletextOtra regla sin motivo propio:
texttrae el texto de la aplicación, en castellano. No debería salir; si te sale, dínoslo.
Envíos
Un envío (shipment) es un porte: los datos del cargador, el transportista, el origen, el destino, la mercancía y el vehículo, los mismos del formulario de emitir. De un envío salen sus documentos, el DeCA y el albarán, que se emiten al crearlo (issue) o más tarde.
Shipment
POST /shipments, GET /shipments/:id y casi todas las llamadas que cambian un envío, y lo que llega en el shipment de cada webhook.Campos
- idstring
- El id del envío.
- referencestring
- Tu referencia.
- statusenum
draft,documented,deliveredocancelled.- routeobject | null
- Si va en una ruta:
{ id, stop }, la ruta y su número de parada. - hourlyboolean
- Si es un servicio por horas (también los de
noteKindSERVICE). - baseDepartureTime, baseReturnTimestring (HH:MM) | null
- Por horas: salida de la base y vuelta a la base, en hora de España. La vuelta se rellena sola al firmar el cliente.
- returnMinutesinteger | null
- Por horas: minutos de vuelta a la base con los que se calcula la vuelta.
- documentsDocument[]
- Sus documentos, del más antiguo al más nuevo. Ver Document.
- attachmentsAttachment[]
- Los papeles del porte que ha subido el conductor. Ver Attachment. En el plan gratis, vacío.
Document
Campos
- id, versionstring, integer
- El id del documento y su versión.
- typeenum
decaodelivery_note.- referencestring
- La referencia del documento (DECA-…, el número del albarán).
- statusenum
activeovoided.- voidReasonstring
- Solo si está anulado: el motivo.
- issuedAt, updatedAtstring (ISO 8601)
- Cuándo se emitió y cuándo cambió por última vez.
- isDemoboolean
- Por la API, siempre false: también en el primer mes los documentos son de verdad. Se queda por compatibilidad.
- publicUrlstring
- Lo que abre el QR: se puede mandar tal cual al conductor.
- signUrlstring
- Solo en el albarán: donde firma el cliente la entrega.
- signatureobject | null
- Solo en el albarán:
{ signedAt, signerName, signerTaxId, remarks }cuando está firmado (cuándo, quién, su DNI y sus reservas), o null. - serviceEndSignatureobject | null
- Solo en el albarán de servicio:
{ signedAt, signerName }de la firma del cliente al acabar el trabajo, o null. - pdfUrlstring
- El PDF de la última versión:
…/api/v1/documents/:id/pdf, con la misma clave.
Crear un envío
POST/shipments
Guarda el porte y, con issue, emite el DeCA, el albarán o los dos en la misma llamada. Con las mismas comprobaciones que en pantalla. Un campo que no conocemos devuelve validation_error, para que una errata no se pierda en silencio.
Cuerpo (JSON)
- referencestring
- Tu número de pedido o porte. Si ya existe un envío con esa referencia, no se crea otro: se devuelve el que hay, con "duplicate": true.
- shipperName, shipperTaxId, shipperAddressstringpara el DeCA
- Cargador contractual (quien contrata el porte): nombre, NIF y domicilio.
- carrierName, carrierTaxIdstring
- Transportista efectivo. Si no los mandas, eres tú (la empresa de la clave).
- carrierPhone, carrierEmailstring
- Para mandarle el QR. No se imprimen.
- originCity, destinationCitystringobligatorio
- Municipio de origen y de destino.
- originName, originAddress, originPostcode, originProvince, originPhone, originContactstring
- El resto del origen.
- destinationName, destinationAddress, destinationPostcode, destinationProvince, destinationPhonestring
- El resto del destino.
- consigneeName, consigneeTaxId, consigneeContact, consigneeContactPhone, consigneeEmailstringpara el albarán
- Destinatario: nombre (obligatorio para el albarán), NIF, persona de contacto, su teléfono y correo.
- clientRefstring
- El número de envío o de albarán que le da tu cliente a esta carga («45255/2026»). Sale impreso en el DeCA y en el albarán. No es la reference: esa es tuya y no se imprime.
- goodsDescriptionstringobligatorio
- La mercancía.
- weightKgnumberobligatorio
- Peso en kilos.
- packagesinteger
- Bultos.
- transportDatestring (YYYY-MM-DD)obligatorio
- Fecha del transporte.
- transportTimestring (HH:MM)
- Hora del transporte.
- platestringobligatorio
- Matrícula de la tractora o del vehículo.
- trailerPlatestring
- Matrícula del remolque o semirremolque.
- vehicleTonnagestring
- Categoría del vehículo («Camión 8 m», «DKV 3,5 t», «Grúa»), hasta 60 caracteres. Solo sale en el albarán, debajo de la matrícula.
- scaleGrossKg, scaleTareKgnumber
- Pesada en báscula (tierras y áridos): peso bruto y tara del ticket de la planta, en kilos; un valor por debajo de 100 se lee como toneladas (
27.92= 27.920 kg). Con los dos,weightKgpasa a ser el neto (bruto − tara) y el DeCA lo lleva como «neto de báscula». En un PATCH, mándalos juntos. - scaleInTime, scaleOutTimestring (HH:MM)
- Hora de entrada y de salida de la báscula. Salen en el albarán, en la caja de la pesada.
- scaleTicketstring
- Número del ticket de báscula de la planta («26SM15862»), hasta 60 caracteres.
- worksitestring
- Obra de destino («Cementerio de Collserola»), hasta 120 caracteres. Solo sale en el albarán.
- dangerousGoodsarray
- Mercancías peligrosas (ADR), las mismas líneas que el eCMR: [{ unNumber, name, class, packingGroup?, tunnelCode?, quantity, unit: «kg» | «l», net?, packageType, packages?, packageCode?, transportCategory?, environment?, waste?, empty?, netExplosiveKg?, fillingPercent? (cisterna, 1–100 %), stop? }]. Obligatorios: cantidad y tipo y número de bultos (o cisterna/granel). Con ellas sale además un documento ADR aparte (type «adr» en documents), que hace de documento de transporte del ADR si lleva todo lo del 5.4.1.1.1 (también el destinatario con su dirección); el DeCA dice «Ver documento ADR-…» y el albarán solo lo que entrega. En una ruta, stop dice a qué parada va cada línea. Con solo el unNumber, name, class, packingGroup, tunnelCode y transportCategory se rellenan desde la Tabla A del ADR 2025 si la tabla no deja dudas. El expedidor responde de los datos.
- adrExemptionboolean
- Se acoge a la exención por cantidades del ADR 1.1.3.6. Pide transportCategory (0 a 4) y quantity en cada línea; vale hasta 1.000 puntos (categoría 1 ×50, 2 ×3, 3 ×1, 4 ×0).
- driverName, driverTaxId, driverPhonestring
- Conductor. Con el teléfono se le puede mandar el QR.
- noteKindenum
- Tipo de albarán:
DELIVERY(entrega, por defecto),PICKUP(recogida) oSERVICE(servicio). - hourlyboolean
- Servicio por horas, de la base a la base: el albarán lleva la salida de la base y la vuelta. Los de
noteKindSERVICElo llevan siempre. - baseDepartureTimestring (HH:MM)
- Por horas: hora de salida de la base, en hora de España.
- returnMinutesinteger (0–600)
- Por horas: lo que se tarda en volver a la base. Al firmar el cliente, la vuelta es la hora de la firma más esto. Si no lo mandas, se usa el de la ficha del cliente (por su NIF, ver
customers[].returnMinutesen la sincronización), y si no hay, 30. - freightTermsenum
- Portes:
PREPAID(pagados) oCOLLECT(debidos). - amountnumber
- Importe en euros.
- terms, notesstring
- Condiciones y observaciones.
- linesarray
- Líneas del albarán:
[{ "description", "reference", "packages", "weightKg" }]. - deliveryNoteNumberstring
- El número de albarán de tu programa. Sin él, lo numera DecaFirma.
- seriesIdstring
- La línea de numeración del albarán (de GET /series).
- deliveryNotePdfstring (base64)
- El PDF del albarán de tu programa, hasta 5 MB. Se firma ese: ver El albarán de tu programa.
- specialAuthorizationboolean
- true si el vehículo circula con autorización especial.
- specialAuthorizationNumberstring
- El número o la referencia de esa autorización (art. 6.e); sale en el DeCA. Solo con
specialAuthorization. - issueenum
deca,delivery_note,bothonone(por defecto: solo se guarda).
- Idempotente por
reference: si ya hay un envío con esa referencia, responde 200 con ese envío y"duplicate": true. Reintentar no duplica nada. - Si no se puede emitir (falta un dato, un NIF mal…), el envío se guarda igual y responde 422 con el envío y
"error": { "code", "message", "details" }(condetails.reasonydetails.document, el que no salió): lo corriges y llamas a /shipments/:id/issue.
curl -X POST https://decafirma.com/api/v1/shipments \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{
"reference": "PED-4521",
"shipperName": "Cerámicas Levante, S.L.",
"shipperTaxId": "B98765431",
"shipperAddress": "Camí Collet 14, 12200 Onda",
"originCity": "Onda",
"destinationCity": "Riba-roja de Túria",
"consigneeName": "Almacenes Pérez, S.A.",
"goodsDescription": "Palés de azulejo",
"weightKg": 9000,
"packages": 12,
"transportDate": "2026-10-05",
"plate": "1234KLM",
"driverName": "Paco Martínez",
"issue": "both"
}'{
"id": "cmu…",
"reference": "PED-4521",
"clientRef": null,
"status": "documented",
"route": null,
"documents": [
{
"id": "cmv…",
"type": "deca",
"reference": "DECA-…",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf"
},
{
"id": "cmw…",
"type": "delivery_note",
"reference": "ALB-2026-0042",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"signUrl": "https://decafirma.com/d/…/firmar",
"signature": null,
"pdfUrl": "https://decafirma.com/api/v1/documents/cmw…/pdf"
}
],
"attachments": []
}Consultar un envío
GET/shipments/:id
El envío, sus documentos (con sus enlaces y si el albarán está firmado) y sus papeles.
Parámetros de ruta
- idstringobligatorio
- El id del envío.
curl https://decafirma.com/api/v1/shipments/cmu… \ -H "Authorization: Bearer dfk_…"
{
"id": "cmu…",
"reference": "PED-4521",
"clientRef": null,
"status": "documented",
"route": null,
"documents": [
{
"id": "cmv…",
"type": "deca",
"reference": "DECA-…",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf"
},
{
"id": "cmw…",
"type": "delivery_note",
"reference": "ALB-2026-0042",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"signUrl": "https://decafirma.com/d/…/firmar",
"signature": null,
"pdfUrl": "https://decafirma.com/api/v1/documents/cmw…/pdf"
}
],
"attachments": []
}Corregir un envío
PATCH/shipments/:id
Como «Cambiar los datos» en pantalla: mandas solo lo que cambia y la versión nueva de sus documentos sale sola, con el mismo QR y lo cambiado impreso. A null se borra un campo; lines, si viene, sustituye a las que hay.
Parámetros de ruta
- idstringobligatorio
- El id del envío.
Cuerpo (JSON)
- …
- Cualquiera de los campos de Crear un envío, todos opcionales.
Devuelve el envío y newVersions, las referencias de los documentos que han salido en versión nueva. Un albarán firmado no se reescribe (unprocessable: se anula y se emite otro), y lo común de una ruta se corrige en todas sus paradas.
curl -X PATCH https://decafirma.com/api/v1/shipments/cmu… \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"weightKg": 9350, "packages": 13}'{
"id": "cmu…",
"reference": "PED-4521",
"status": "documented",
…,
"newVersions": ["DECA-…", "ALB-2026-0042"]
}Emitir un documento
POST/shipments/:id/issue
Emite el DeCA o el albarán de un envío ya guardado: si lo creaste sin emitir o faltaba algo.
Parámetros de ruta
- idstringobligatorio
- El id del envío.
Cuerpo (JSON)
- typeenumobligatorio
decaodelivery_note.- deliveryNoteNumberstring
- Solo el albarán: el número de tu programa.
- seriesIdstring
- Solo el albarán: la línea de numeración.
- deliveryNotePdfstring (base64)
- Solo el albarán: el PDF de tu programa, hasta 5 MB.
Si no se puede emitir, 422 con el envío y "error": { "code", "message", "details" }: el motivo, en details.reason.
curl -X POST https://decafirma.com/api/v1/shipments/cmu…/issue \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"type": "deca"}'{
"id": "cmu…",
"reference": "PED-4521",
"clientRef": null,
"status": "documented",
"route": null,
"documents": [
{
"id": "cmv…",
"type": "deca",
"reference": "DECA-…",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf"
},
{
"id": "cmw…",
"type": "delivery_note",
"reference": "ALB-2026-0042",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"signUrl": "https://decafirma.com/d/…/firmar",
"signature": null,
"pdfUrl": "https://decafirma.com/api/v1/documents/cmw…/pdf"
}
],
"attachments": []
}Cambiar de camión
PATCH/shipments/:id/vehicle
El camión cambia en ruta (una avería, un transbordo): sale la versión nueva con la matrícula nueva y el motivo. En una ruta, para todas sus paradas.
Parámetros de ruta
- idstringobligatorio
- El id del envío.
Cuerpo (JSON)
- platestringobligatorio
- La matrícula nueva.
- trailerPlatestring
- El remolque nuevo.
- reasonstring
- Por qué cambia («Avería en Zaragoza»).
Si no se puede, 422 con el envío y "error": { "code", "message", "details" }: el motivo, en details.reason.
curl -X PATCH https://decafirma.com/api/v1/shipments/cmu…/vehicle \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"plate": "9034MCC", "reason": "Avería en Zaragoza"}'{
"id": "cmu…",
"reference": "PED-4521",
"clientRef": null,
"status": "documented",
"route": null,
"documents": [
{
"id": "cmv…",
"type": "deca",
"reference": "DECA-…",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf"
},
{
"id": "cmw…",
"type": "delivery_note",
"reference": "ALB-2026-0042",
"version": 1,
"status": "active",
"issuedAt": "2026-10-05T07:12:00Z",
"updatedAt": "2026-10-05T07:12:00Z",
"isDemo": false,
"publicUrl": "https://decafirma.com/d/…",
"signUrl": "https://decafirma.com/d/…/firmar",
"signature": null,
"pdfUrl": "https://decafirma.com/api/v1/documents/cmw…/pdf"
}
],
"attachments": []
}El albarán de tu programa
Si tu programa ya hace el albarán (el de Odoo, por ejemplo), mándalo en deliveryNotePdf al emitir el albarán, en POST /shipments o en POST /shipments/:id/issue. Se firma ese: el PDF de tu programa va delante tal cual, y detrás nuestra hoja con el QR para firmar.
Al firmar, esa hoja recoge la conformidad (nombre, DNI, fecha y hora, reservas y la firma) y las fotos, y en cada hoja del tuyo sale al pie dónde está la firma. Cuando llega el aviso delivery_note.signed, GET /documents/:id/pdf te da el PDF firmado para guardarlo junto al albarán en tu programa.
En las rutas, igual: deliveryNotePdf en cada parada de POST /routes y de POST /routes/:id/stops (con "deliveryNotes": true), o "pdfs": { "<shipmentId>": "…" } en POST /routes/:id/delivery-notes. La parada sin PDF lleva nuestro albarán. Hasta 5 MB cada uno y 40 MB entre todos los de una llamada.
curl -X POST https://decafirma.com/api/v1/shipments/cms…/issue \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d "{\"type\": \"delivery_note\", \"deliveryNoteNumber\": \"WH/OUT/00042\", \"deliveryNotePdf\": \"$(base64 -w0 albaran.pdf)\"}"Documentos
Los DeCA y los albaranes emitidos. Se consultan dentro de su envío; aquí, la lista de lo que ha cambiado, el PDF, anular y mandarlo por correo.
Lo que ha cambiado
GET/documents
Los documentos emitidos, en versión nueva, anulados o firmados desde una fecha, del más antiguo al más nuevo. Para ponerte al día sin preguntar envío por envío: guarda until y pásalo como since la próxima vez. Con hasMore a true, vuelve a llamar.
Parámetros de consulta
- sincestring (ISO 8601)
- Desde cuándo. Por defecto, hace 7 días.
- typeenum
decaodelivery_note. Sin él, los dos.- limitinteger
- Cuántos como mucho, de 1 a 500. Por defecto, 100.
curl "https://decafirma.com/api/v1/documents?since=2026-10-07T00:00:00Z&limit=100" \ -H "Authorization: Bearer dfk_…"
{
"since": "2026-10-07T00:00:00.000Z",
"until": "2026-10-07T10:42:00.000Z",
"hasMore": false,
"data": [
{
"id": "cmw…", "shipmentId": "cmu…", "routeId": null,
"type": "delivery_note", "reference": "ALB-2026-0042",
"version": 2, "status": "active", "signed": true,
"changedAt": "2026-10-07T10:42:00.000Z"
}
]
}Descargar el PDF
GET/documents/:id/pdf
El PDF de la última versión, con su QR. Si el albarán está firmado, el firmado.
Parámetros de ruta
- idstringobligatorio
- El id del documento.
curl https://decafirma.com/api/v1/documents/cmv…/pdf \ -H "Authorization: Bearer dfk_…" \ -o DECA.pdf
Content-Type: application/pdf %PDF-1.7 …
Anular un documento
POST/documents/:id/void
Queda la versión con la marca de anulado y el motivo. Devuelve el envío.
Parámetros de ruta
- idstringobligatorio
- El id del documento.
Cuerpo (JSON)
- reasonstringobligatorio
- Por qué se anula.
curl -X POST https://decafirma.com/api/v1/documents/cmv…/void \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"reason": "El cliente cancela el porte"}'{
"id": "cmu…",
"reference": "PED-4521",
"documents": [
{ "id": "cmv…", "type": "deca", "status": "voided",
"voidReason": "El cliente cancela el porte", … }
],
…
}Mandarlo por correo
POST/documents/:id/send
Se lo manda al conductor o al cliente, con el enlace y el PDF.
Parámetros de ruta
- idstringobligatorio
- El id del documento.
Cuerpo (JSON)
- emailstringobligatorio
- A quién.
- includeRelatedboolean
- true para mandar también el otro papel del mismo porte (el albarán con el DeCA, o al revés).
curl -X POST https://decafirma.com/api/v1/documents/cmv…/send \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"email": "paco@transportes.es", "includeRelated": true}'{
"sent": true,
"to": "paco@transportes.es",
"reference": "DECA-…"
}Rutas
Una ruta de reparto: un solo DeCA para todas las paradas del mismo cliente y el mismo transportista (Resolución de 5 de junio de 2026, apartado sexto) y, si quieres, un albarán por parada. Cada parada es un envío.
Route
Campos
Crear una ruta
POST/routes
Lo común va en common (los campos del envío que comparten todas las paradas) y la base es de donde sale el camión. La misma reference no crea otra ruta: devuelve la que hay, con "duplicate": true.
Cuerpo (JSON)
- referencestring
- Tu referencia de la ruta.
- commonobject
- Los campos de Shipment comunes a todas las paradas (cargador, transportista, fecha, vehículo, conductor…) y
base. - common.baseobject
- De donde sale el camión:
{ name, address, postcode, city, province }. - stopsarrayobligatorio
- Las paradas, en orden. Campos de cada una, abajo.
- deliveryNotesboolean
- true para emitir también un albarán por parada.
- seriesIdstring
- La línea de numeración de esos albaranes.
- fromDocumentIdstring
- El id de un DeCA suelto: pasa a ser la parada 1 de la ruta nueva, en versión nueva del mismo DeCA.
Cada parada (stops[])
- typeenumobligatorio
delivery(entrega) opickup(recogida).- locationobjectobligatorio
- El lugar de la parada:
{ name, address, postcode, city, province }. - consigneeName, consigneeTaxIdstring
- Destinatario de esa parada y su NIF.
- contact, phone, emailstring
- Persona de contacto, teléfono y correo en la parada.
- goodsDescription, packages, weightKgstring, integer, number
- Mercancía, bultos y kilos de la parada.
- clientRefstring
- El número de envío o albarán del cliente en esa parada («999908049810»). Sale en la relación de envíos del DeCA, debajo del destino, y en el albarán de la parada.
- deliveryNoteNumberstring
- El número del albarán de esa parada en tu programa.
- deliveryNotePdfstring (base64)
- El PDF de tu programa para esa parada (hasta 5 MB). Sin él, lleva nuestro albarán.
curl -X POST https://decafirma.com/api/v1/routes \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{
"reference": "REPARTO-0710",
"common": {
"shipperName": "Distribuciones Montseny, S.A.",
"shipperTaxId": "A08015497",
"transportDate": "2026-10-07",
"plate": "4821KLM",
"driverName": "Antonio Ruiz",
"base": { "name": "Nave Montseny", "city": "Granollers", "postcode": "08400" }
},
"stops": [
{ "type": "delivery",
"location": { "name": "Ferretería Central", "city": "Granollers" },
"consigneeName": "Ferretería Central, S.L.",
"goodsDescription": "Palés", "packages": 6, "weightKg": 3200,
"clientRef": "999908049810", "deliveryNoteNumber": "05401" },
{ "type": "pickup",
"location": { "name": "Almacenes Pérez", "city": "Sabadell" },
"goodsDescription": "Envases vacíos", "weightKg": 400 }
],
"deliveryNotes": true
}'{
"id": "cmr…",
"reference": "REPARTO-0710",
"deca": {
"id": "cmv…", "reference": "DECA-…", "version": 1, "isDemo": false,
"status": "active", "publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf", "updatedAt": "2026-10-07T06:30:00Z"
},
"stops": [
{ "id": "cmu…", "reference": "REPARTO-0710-1", "status": "documented",
"route": { "id": "cmr…", "stop": 1 }, "documents": [ … ], "attachments": [] },
{ "id": "cmu…", "reference": "REPARTO-0710-2", "status": "documented",
"route": { "id": "cmr…", "stop": 2 }, "documents": [ … ], "attachments": [] }
]
}Consultar una ruta
GET/routes/:id
La ruta, su DeCA y cada parada con sus documentos.
Parámetros de ruta
- idstringobligatorio
- El id de la ruta.
curl https://decafirma.com/api/v1/routes/cmr… \ -H "Authorization: Bearer dfk_…"
{
"id": "cmr…",
"reference": "REPARTO-0710",
"deca": {
"id": "cmv…", "reference": "DECA-…", "version": 1, "isDemo": false,
"status": "active", "publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf", "updatedAt": "2026-10-07T06:30:00Z"
},
"stops": [
{ "id": "cmu…", "reference": "REPARTO-0710-1", "status": "documented",
"route": { "id": "cmr…", "stop": 1 }, "documents": [ … ], "attachments": [] },
{ "id": "cmu…", "reference": "REPARTO-0710-2", "status": "documented",
"route": { "id": "cmr…", "stop": 2 }, "documents": [ … ], "attachments": [] }
]
}Añadir paradas
POST/routes/:id/stops
Con el camión en marcha: el DeCA sale en versión nueva, con el mismo QR.
Parámetros de ruta
- idstringobligatorio
- El id de la ruta.
Cuerpo (JSON)
- stopsarrayobligatorio
- Las paradas nuevas, con los campos de Crear una ruta.
- deliveryNotesboolean
- true para emitir su albarán.
- seriesIdstring
- La línea de numeración de esos albaranes.
curl -X POST https://decafirma.com/api/v1/routes/cmr…/stops \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"stops": [ … ], "deliveryNotes": true}'{
"id": "cmr…",
"reference": "REPARTO-0710",
"deca": {
"id": "cmv…", "reference": "DECA-…", "version": 1, "isDemo": false,
"status": "active", "publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf", "updatedAt": "2026-10-07T06:30:00Z"
},
"stops": [
{ "id": "cmu…", "reference": "REPARTO-0710-1", "status": "documented",
"route": { "id": "cmr…", "stop": 1 }, "documents": [ … ], "attachments": [] },
{ "id": "cmu…", "reference": "REPARTO-0710-2", "status": "documented",
"route": { "id": "cmr…", "stop": 2 }, "documents": [ … ], "attachments": [] }
]
}Quitar una parada
DELETE/routes/:id/stops/:shipmentId
Si su albarán está firmado, no se puede (unprocessable). Devuelve la ruta o, si se queda sin paradas, { id, deleted: true }.
Parámetros de ruta
- idstringobligatorio
- El id de la ruta.
- shipmentIdstringobligatorio
- El id del envío de esa parada.
curl -X DELETE https://decafirma.com/api/v1/routes/cmr…/stops/cmu… \ -H "Authorization: Bearer dfk_…"
{
"id": "cmr…",
"reference": "REPARTO-0710",
"deca": {
"id": "cmv…", "reference": "DECA-…", "version": 1, "isDemo": false,
"status": "active", "publicUrl": "https://decafirma.com/d/…",
"pdfUrl": "https://decafirma.com/api/v1/documents/cmv…/pdf", "updatedAt": "2026-10-07T06:30:00Z"
},
"stops": [
{ "id": "cmu…", "reference": "REPARTO-0710-1", "status": "documented",
"route": { "id": "cmr…", "stop": 1 }, "documents": [ … ], "attachments": [] },
{ "id": "cmu…", "reference": "REPARTO-0710-2", "status": "documented",
"route": { "id": "cmr…", "stop": 2 }, "documents": [ … ], "attachments": [] }
]
}Albaranes de las paradas
POST/routes/:id/delivery-notes
Emite el albarán de las paradas que no lo tienen: con tus números, con una línea de numeración o con el PDF de tu programa.
Parámetros de ruta
- idstringobligatorio
- El id de la ruta.
Cuerpo (JSON)
- seriesIdstring
- La línea de numeración.
- numbersobject
- El número de cada parada:
{ "<shipmentId>": "05402" }. - pdfsobject
- El PDF de tu programa por parada, en base64:
{ "<shipmentId>": "JVBERi0…" }. Hasta 5 MB cada uno y 40 MB en total.
created trae las referencias de los albaranes emitidos y skipped, cada parada que se ha saltado con su shipmentId, su reason y sus datos (los de la tabla de motivos) y un message en inglés. Las paradas que ya tienen albarán no salen en ninguna de las dos.
curl -X POST https://decafirma.com/api/v1/routes/cmr…/delivery-notes \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{
"numbers": { "cmu…1": "05402", "cmu…2": "05403" },
"pdfs": { "cmu…1": "JVBERi0…" }
}'{
"created": ["05402", "05403"],
"skipped": [
{
"shipmentId": "cmu…3",
"reason": "missing_delivery_note_number",
"message": "Missing its delivery note number in «numbers»."
}
]
}Series
Las líneas de numeración de tus albaranes, las mismas de Configuración.
Tus numeraciones
GET/series
Cada línea con el número que toca. Al emitir, seriesId para usar una, o deliveryNoteNumber con el número de tu programa de facturación.
defaultNextNumber es el siguiente de la numeración de siempre; manualNumberingDefault, si tu empresa pone el número a mano por defecto, y nextManualNumber, el que te sugerimos en ese caso.
curl https://decafirma.com/api/v1/series \ -H "Authorization: Bearer dfk_…"
{
"data": [
{ "id": "cms…", "name": "Albaranes Onda", "nextNumber": "ON-2026-0018" }
],
"defaultNextNumber": "ALB-2026-0043",
"manualNumberingDefault": false,
"nextManualNumber": "05404"
}CMR internacional (eCMR)
La carta de porte internacional electrónica, con el módulo eCMR (en la prueba va dentro). Se emite con sus datos y te devolvemos los enlaces de firma de cada parte, en este orden: el remitente al cargar, el transportista al hacerse cargo y el destinatario al recibir. Cada firma deja versión nueva con su huella SHA-256 y te llega el aviso cmr.signed. Los países van en ISO de dos letras; si uno no ha ratificado el Protocolo e-CMR, el PDF avisa de llevar también el CMR en papel.
Emitir un CMR
POST/cmr
Con externalReference repetida y un CMR vivo, devuelve ese con duplicate: true en vez de otro.
Sin el módulo ni su prueba de 7 días: 422 con ecmr_addon_required (acabada la prueba, lo ya emitido se sigue consultando y firmando). Un porte dentro de España lleva DeCA, no CMR. goods.dangerous (ADR, con cantidad y tipo y número de bultos, y nameEn si no es solo entre España y Portugal) genera además el documento ADR aparte, que la respuesta trae en adrDocument; en la casilla 9 va «Ver documento ADR-…» y un resumen. successiveCarriers admite texto o lista (casilla 17); la 16 eres tú, el transportista contractual, y el chófer del sucesivo puede firmar en su nombre (onBehalfOf). La casilla 20 sale de paymentTerms (pagado, el remitente; debido, el destinatario) salvo que mandes payer. La 21, tu ciudad y la fecha de emisión. Un CMR por externalReference.
curl -X POST https://decafirma.com/api/v1/cmr \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{
"externalReference": "SO-2026-0412",
"sender": { "name": "Estampaciones Vallès SL", "taxId": "B12345678", "address": "C/ Industria 4, 08202 Sabadell", "country": "ES" },
"consignee": { "name": "Metalúrgica Lisboa Lda", "taxId": "PT509876543", "address": "Rua do Porto 12, 1100 Lisboa", "country": "PT" },
"takingOver": { "place": "Polígono Can Roqueta, Sabadell", "country": "ES" },
"takingOverDate": "2026-10-28",
"delivery": { "place": "Rua do Porto 12, Lisboa", "country": "PT" },
"goods": { "nature": "Piezas de acero", "packages": 12, "packing": "Palets", "grossWeightKg": 8400, "volumeM3": 22.5,
"dangerous": [{ "unNumber": "1203", "name": "Gasolina", "nameEn": "Gasoline", "class": "3", "packingGroup": "II", "tunnelCode": "D/E", "quantity": 1200, "unit": "l", "packageType": "cisterna" }] },
"successiveCarriers": [{ "name": "Transviaro SL", "taxId": "B58870676", "country": "ES", "plate": "1234ABC" }],
"payer": { "name": "Agencia que paga SL", "taxId": "B…" },
"vehicle": { "plate": "7031HBS", "trailerPlate": "R1234BCD", "driverName": "Daniel Rodríguez" },
"paymentTerms": "prepaid",
"senderInstructions": "Despacho en aduana en destino"
}'{
"id": "cmv…", "type": "cmr", "reference": "CMR-20261028-K4X3EA", "version": 1, "status": "active",
"demo": false, "eftiId": "5b0c…", "pdfUrl": "https://decafirma.com/d/…",
"signatures": { "sender": null, "carrier": null, "consignee": null },
"signUrls": {
"sender": "https://decafirma.com/c/…/firmar?parte=remitente&k=…",
"carrier": "https://decafirma.com/c/…/firmar?parte=transportista&k=…",
"consignee": "https://decafirma.com/c/…/firmar?parte=destinatario&k=…"
},
"nextSigner": "carrier",
"versions": [{ "version": 1, "sha256": "3f2a…", "reason": null, "createdAt": "…" }]
}Un CMR, con sus firmas
GET/cmr/{id}
Cada firma trae nombre, documento, hora, reservas, ubicación (si el móvil la dio) y la versión que dejó. GET /cmr?since=… lista los cambiados desde una fecha.
curl https://decafirma.com/api/v1/cmr/cmv… \ -H "Authorization: Bearer dfk_…"
{ "id": "cmv…", "status": "delivered", "version": 4, "signatures": { "consignee": { "name": "João Silva", "signedAt": "…", "reservations": "Un palet golpeado", "location": { "lat": 38.72, "lng": -9.14, "accuracyM": 12 }, "notSigned": false, "version": 4 } } }El remitente o el destinatario no firma
POST/cmr/{id}/not-signed
El orden es obligatorio: primero el remitente, luego el transportista y al final el destinatario (cmr_wrong_order si no). Si el remitente o el destinatario no firma, se deja constancia con el motivo: versión nueva, aviso cmr.signed y, en su firma, state: refused con reason y recordedBy.
curl -X POST https://decafirma.com/api/v1/cmr/cmv…/not-signed \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{ "party": "sender", "reason": "No había nadie en el muelle" }'{ "id": "cmv…", "version": 2, "nextSigner": "carrier", "signatures": { "sender": { "state": "refused", "reason": "No había nadie en el muelle", "recordedBy": "Olga (API)" } } }El PDF vigente, en binario
GET/cmr/{id}/pdf
Para guardarlo en tu pedido. Lleva la versión y la huella en X-Cmr-Version y X-Cmr-Sha256.
curl https://decafirma.com/api/v1/cmr/cmv…/pdf \ -H "Authorization: Bearer dfk_…" -o cmr.pdf
HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="CMR-20261028-K4X3EA-v4.pdf" X-Cmr-Version: 4 X-Cmr-Sha256: 7e41aaae…
Cambiar antes de la entrega
PATCH/cmr/{id}
Solo deliveryPlace, senderInstructions, documentsAttached, specialAgreements y successiveCarriers, con reason obligatorio. Cada cambio queda con su valor de antes y sale versión nueva.
curl -X PATCH https://decafirma.com/api/v1/cmr/cmv… \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{ "deliveryPlace": "Muelle 3, Rua do Porto 12, Lisboa", "reason": "El cliente cambia el muelle" }'{ "id": "cmv…", "version": 2, "status": "active" }Papeles del porte
Los papeles del porte: las fotos que el conductor sube del albarán del cargador, la carta de porte o el ticket de báscula, ya leídas (número, quién lo emite, fecha y peso neto). Van en attachments de cada envío y en el shipment de cada webhook; al subirse uno llega attachment.received. En el plan gratis no hay papeles: la lista va vacía.
Attachment
Campos
- idstring
- El id del papel.
- typeenum | null
delivery_note,consignment_note,weighbridge_ticket,ticket,othero null.- numberstring | null
- El número que trae el papel.
- issuerstring | null
- Quién lo emite.
- datestring | null
- Su fecha (YYYY-MM-DD).
- netWeightKgnumber | null
- Peso neto en kilos.
- parsedboolean
- false si no se pudo leer: la foto llega igual y los datos los pone la oficina.
- mimeTypestring
- image/jpeg, application/pdf…
- uploadedBy, uploadedAtstring
- Quién lo subió y cuándo.
- invoicedAtstring | null
- Cuándo se marcó facturado, o null.
- fileUrlstring
- La foto o el PDF:
…/api/v1/attachments/:id/file.
Los papeles nuevos
GET/attachments
Los subidos o cambiados desde una fecha. Como en Documents: guarda until y pásalo como since.
Parámetros de consulta
- sincestring (ISO 8601)
- Desde cuándo.
- limitinteger
- Cuántos como mucho, de 1 a 500. Por defecto, 100.
curl "https://decafirma.com/api/v1/attachments?since=2026-10-07T00:00:00Z&limit=100" \ -H "Authorization: Bearer dfk_…"
{
"since": "2026-10-07T00:00:00.000Z",
"until": "2026-10-07T16:05:00.000Z",
"hasMore": false,
"data": [
{ …el papel…, "shipmentId": "cmu…", "changedAt": "2026-10-07T16:05:00.000Z" }
]
}Descargar la foto
GET/attachments/:id/file
La foto (o el PDF), con su Content-Type.
Parámetros de ruta
- idstringobligatorio
- El id del papel.
curl https://decafirma.com/api/v1/attachments/cmp…/file \ -H "Authorization: Bearer dfk_…" \ -o papel.jpg
Content-Type: image/jpeg …
Marcarlo facturado
POST/attachments/:id/invoiced
Como en la lista de Papeles: lo marca facturado o, con false, lo desmarca. Devuelve el papel.
Parámetros de ruta
- idstringobligatorio
- El id del papel.
Cuerpo (JSON)
- invoicedbooleanobligatorio
- true o false.
curl -X POST https://decafirma.com/api/v1/attachments/cmp…/invoiced \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"invoiced": true}'{
"id": "cmp…", "type": "consignment_note", "number": "26-004417",
"issuer": "ÁRIDOS DEL VALLÈS, S.L.", "date": "2026-10-07", "netWeightKg": 26740,
"parsed": true, "mimeType": "image/jpeg", "uploadedBy": "Antonio",
"uploadedAt": "2026-10-07T16:05:00Z", "invoicedAt": "2026-10-08T09:00:00Z",
"fileUrl": "https://decafirma.com/api/v1/attachments/cmp…/file"
}Lectura de encargos
Leer un encargo: del WhatsApp o el correo del cliente, a los campos de un envío.
Leer un encargo
POST/parse
Le pasas el texto y te devuelve lo que entiende (cargador, origen, destino, mercancía, fecha y hora…), sin crear nada, para que tu programa lo coloque en sus campos. Si el texto no encarga ningún porte, 422 con not_a_shipment.
Cuerpo (JSON)
- textstringobligatorio
- El mensaje del cliente, tal cual.
uncertainFields trae las rutas de los campos dudosos ("shipper.name"), para que tu programa los marque. shipmentCount dice cuántos portes encarga el texto.
curl -X POST https://decafirma.com/api/v1/parse \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"text": "Mañana a las 8 recoger 12 palés de azulejo en Cerámicas Levante (Onda) para Almacenes Pérez en Riba-roja, unos 9.000 kg"}'{
"extraction": {
"isTransportDocument": true,
"shipper": { "name": "Cerámicas Levante", "taxId": null, "address": null },
"carrier": { "name": null, "taxId": null, "phone": null },
"origin": { "name": "Cerámicas Levante", "address": null,
"postcode": null, "city": "Onda", "province": null },
"destination": { "name": "Almacenes Pérez", "address": null,
"postcode": null, "city": "Riba-roja de Túria", "province": null },
"consignee": { "name": "Almacenes Pérez", "taxId": null,
"contact": null, "phone": null },
"goodsDescription": "Palés de azulejo",
"packages": 12,
"weightKg": 9000,
"date": "2026-10-18",
"time": "08:00",
"plate": null,
"trailerPlate": null,
"driver": { "name": null, "taxId": null },
"deliveryNoteNumber": null,
"notes": null,
"shipmentCount": 1,
"uncertainFields": ["consignee.name"],
"unloading": { "date": null, "time": null }
}
}Puesta a punto
Deja DecaFirma como tu programa (Odoo, tu ERP): series, clientes, lugares, equipo y vehículos, cada uno con su id de tu programa en externalId. Manda tu programa, pero nunca se borra nada: archived: true lo da de baja. En la aplicación sale con la marca «de tu programa». Ni correos ni WhatsApp, salvo la invitación que pidas con invite.
Poner a punto
POST/sync
Cada elemento se empareja por su externalId; si no lo tiene ninguno, por la clave de siempre (clientes y personas por NIF, o por nombre si no tienen; vehículos por matrícula; lugares por nombre y municipio, sin mayúsculas ni acentos; series por nombre o formato), y lo que ya metiste a mano se queda con tu externalId en vez de salir repetido. Si tampoco, se crea. Un campo que no mandas no se toca; null (o false, como manda Odoo los vacíos) lo vacía.
Se aplica en este orden: company, settings, series, customers, places, people y vehicles, así que un vehículo puede llevar de conductor a una persona de la misma llamada. Hasta 500 elementos por llamada (si no, 413): manda los demás en otra. company, settings, series y people piden una clave de un administrador.
Cuerpo (JSON)
- dryRunboolean
- true: no guarda nada y contesta lo que haría.
- companyobject
legalName,tradeName,phone,email,address,postcode,city,province: lo que llega con valor pisa lo de DecaFirma; vacío, no borra.taxIdsolo se comprueba: el NIF de la empresa no cambia nunca.- settingsobject
manualDeliveryNoteNumber,askForAttachments,hourlyServices(true/false) ydeliveryNoteFormat, el formato de la serie por defecto (ALB[YY]%%%%%).- series[]array
externalId,name,format,default,nextNumber(solo sube el contador, nunca repite números) yarchived.- customers[]array
externalId,name,taxId,address,postcode,city,province,phone,email,billedHourly,returnMinutes,archived.- places[]array
externalId,name,address,postcode,city,province,phone,archived.- people[]array
externalId,kind(driver,carrier,office),name,taxId,phone,email,role(admin,issuer,viewer: el que tendrá al entrar),drives,issuesDeca,issuesDeliveryNote,editsDeca,editsDeliveryNote,employerExternalId(el transportista del que es),inviteyarchived.- vehicles[]array
externalId,plate,type,description,driverExternalId(quien lo lleva; el camión de un autónomo, con él) yarchived.
Solo vienen las listas que mandas. ids trae el id en DecaFirma de cada externalId (null en un dryRun de algo que aún no existe). Lo que no se puede aplicar no para lo demás: va en skipped con su reason (los de la tabla de motivos), sus datos y un message en inglés. company y settings contestan updated (lo que cambió) y skipped, con field. Un archived de algo que no existe no lo crea. Mandar lo mismo otra vez no cambia nada: todo sale en unchanged.
curl -X POST https://decafirma.com/api/v1/sync -H "Authorization: Bearer dfk_…" -H "Content-Type: application/json" -d '{
"customers": [
{ "externalId": "odoo:res.partner:42", "name": "Cerámicas Levante SL",
"taxId": "B12345674", "city": "Onda" }
],
"people": [
{ "externalId": "odoo:res.partner:77", "kind": "carrier",
"name": "Pablo Martín", "taxId": "12345678Z" }
],
"vehicles": [
{ "externalId": "odoo:fleet.vehicle:3", "plate": "1234KLM",
"driverExternalId": "odoo:res.partner:77" }
]
}'{
"dryRun": false,
"customers": {
"created": 1, "updated": 0, "unchanged": 0, "archived": 0,
"skipped": [],
"ids": { "odoo:res.partner:42": "cmc…" }
},
"people": {
"created": 0, "updated": 1, "unchanged": 0, "archived": 0,
"skipped": [], "invited": 0,
"ids": { "odoo:res.partner:77": "cmp…" }
},
"vehicles": {
"created": 0, "updated": 0, "unchanged": 0, "archived": 0,
"skipped": [
{ "externalId": "odoo:fleet.vehicle:3", "reason": "plate_taken",
"message": "The plate 1234KLM already belongs to another vehicle (externalId «odoo:fleet.vehicle:9»).",
"plate": "1234KLM" }
],
"ids": {}
}
}Lo que hay
GET/sync
Lo que hay en DecaFirma, también lo dado de baja, con su externalId (null si no viene de tu programa), para enlazar lo que ya existía antes de la primera puesta a punto.
curl https://decafirma.com/api/v1/sync -H "Authorization: Bearer dfk_…"
{
"customers": [{ "id": "cmc…", "externalId": "odoo:res.partner:42", "name": "Cerámicas Levante SL",
"taxId": "B12345674", "city": "Onda", "archived": false }],
"places": [{ "id": "cml…", "externalId": null, "name": "Nave 8", "city": "Polinyà", "archived": false }],
"people": [{ "id": "cmp…", "externalId": "odoo:res.partner:77", "name": "Pablo Martín", "kind": "carrier",
"taxId": "12345678Z", "employerExternalId": null, "hasAccount": false, "archived": false }],
"vehicles": [{ "id": "cmv…", "externalId": null, "plate": "1234KLM", "driverId": "cmp…",
"driverExternalId": "odoo:res.partner:77", "archived": false }],
"series": [{ "id": "cms…", "externalId": null, "name": "General", "format": "ALB[YY]%%%%%",
"default": true, "archived": false }]
}Avisos (webhooks)
Cada vez que un documento se emite, sale en versión nueva, se anula o se firma (en pantalla, por la API o en el móvil de quien firma), o el conductor sube un papel, te mandamos un POST con el envío entero. La dirección se pone en Configuración Conectar tu programa (API) o con POST /webhook.
Eventos, cabeceras y cuerpo
Eventos
- document.issued
- Se ha emitido un DeCA o un albarán.
- document.updated
- Ha salido una versión nueva (una corrección, un cambio de camión, una parada nueva).
- document.voided
- Se ha anulado.
- delivery_note.signed
- El cliente ha firmado el albarán:
GET /documents/:id/pdfda el PDF firmado. - service_note.signed
- Se ha firmado un albarán de servicio.
- cmr.signed
- Ha firmado una de las tres partes de un CMR (remitente, transportista o destinatario).
- attachment.received
- El conductor ha subido un papel del porte; llega en
attachment. - test
- El de prueba de
POST /webhook/test, conmessage.
Cabeceras
- X-DecaFirma-Eventstring
- El evento.
- X-DecaFirma-Signaturestring
sha256=y el HMAC-SHA256 del cuerpo crudo con tu secreto, en hexadecimal.- X-DecaFirma-Deliverystring
- El id de este aviso.
Cuerpo
- typestring
- El evento.
- createdAtstring (ISO 8601)
- Cuándo pasó.
- documentobject
{ id, type, reference, version, status }del documento que cambia.- routeIdstring | null
- La ruta, si el documento es de una.
- shipmentShipment
- El envío entero, como en
GET /shipments/:id(con lasignaturedel albarán al firmarse). - attachmentAttachment
- Solo en
attachment.received: el papel. - messagestring
- Solo en
test.
Un aviso que no llega no se reintenta
until.POST https://tu-programa.es/decafirma
Content-Type: application/json
X-DecaFirma-Event: delivery_note.signed
X-DecaFirma-Signature: sha256=5f1c…
X-DecaFirma-Delivery: cmx…
{
"type": "delivery_note.signed",
"createdAt": "2026-10-07T10:42:00Z",
"document": { "id": "cmw…", "type": "delivery_note",
"reference": "ALB-2026-0042", "version": 2, "status": "active" },
"routeId": null,
"shipment": {
"id": "cmu…", "reference": "PED-4521", "status": "delivered",
"documents": [ { …, "signature": { "signedAt": "2026-10-07T10:41:30Z",
"signerName": "Luis Gómez", "signerTaxId": "12345678Z", "remarks": null } } ],
…
}
}Comprobar la firma
Antes de fiarte del cuerpo, calcula el HMAC-SHA256 del cuerpo crudo (los bytes tal cual llegan, antes de parsear el JSON) con tu secreto, en hexadecimal, y compáralo con lo que va detrás de sha256= en X-DecaFirma-Signature, con una comparación de tiempo constante. Si no coincide, descártalo.
Contesta con un 2xx en cuanto lo tengas; lo pesado, después. Con X-DecaFirma-Delivery sabes si ya lo habías procesado.
import crypto from 'node:crypto'
// raw: el cuerpo tal cual (Buffer), p. ej. express.raw({ type: 'application/json' })
function firmaBuena(raw, cabecera, secreto) {
const esperada = 'sha256=' +
crypto.createHmac('sha256', secreto).update(raw).digest('hex')
const a = Buffer.from(esperada)
const b = Buffer.from(cabecera ?? '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}import hashlib, hmac
def firma_buena(raw: bytes, cabecera: str, secreto: str) -> bool:
esperada = "sha256=" + hmac.new(
secreto.encode(), raw, hashlib.sha256
).hexdigest()
return hmac.compare_digest(esperada, cabecera or "")Poner la dirección
POST/webhook
Lo mismo que en Configuración: una dirección https y te damos el secreto con el que firmamos los avisos. Cada llamada cambia el secreto: el que vale es siempre el último, así que guárdalo antes de pedir la prueba.
Cuerpo (JSON)
- urlstringobligatorio
- Tu dirección https.
curl -X POST https://decafirma.com/api/v1/webhook \
-H "Authorization: Bearer dfk_…" \
-H "Content-Type: application/json" \
-d '{"url": "https://tu-programa.es/decafirma"}'{
"url": "https://tu-programa.es/decafirma",
"secret": "whsec_…"
}Ver cómo va
GET/webhook
La dirección, cuándo salió el último aviso y el último error, si lo hubo.
curl https://decafirma.com/api/v1/webhook \ -H "Authorization: Bearer dfk_…"
{
"url": "https://tu-programa.es/decafirma",
"lastDeliveryAt": "2026-10-07T10:42:00.000Z",
"lastError": null
}Quitarlo
DELETE/webhook
Dejan de salir avisos.
curl -X DELETE https://decafirma.com/api/v1/webhook \ -H "Authorization: Bearer dfk_…"
{ "deleted": true }Mandar uno de prueba
POST/webhook/test
Manda el evento test, firmado, a tu dirección y te dice qué ha contestado. Si no llega o no contesta un 2xx, responde 422 con ok a false y el motivo en detail.
curl -X POST https://decafirma.com/api/v1/webhook/test \ -H "Authorization: Bearer dfk_…"
{
"ok": true,
"detail": "HTTP 200"
}