Referencia de la API · v1

Conecta tu ERP y emite sin teclear

Tu programa manda el porte, DecaFirma emite el DeCA y el albarán con su QR y te devuelve el enlace y el PDF. Lo mismo que en pantalla, con las mismas comprobaciones.

Base URL
https://decafirma.com/api/v1
Autenticación
Authorization: Bearer dfk_…
Índice de la API
Introducción

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).

Parte de arriba del PDF de un DeCA de ejemplo: el título, la referencia, quién lo emite y el código QR con su dirección web
Lo que baja 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

Si ya hay un envío (o una ruta) con esa reference, no se crea otro: se devuelve el que hay, con "duplicate": true. Reintentar una llamada que se cortó no duplica nada.

Primer mes

El primer mes, los documentos que emites por la API son de verdad, como los de pantalla: valen ante la Inspección y llevan "isDemo": false.
Las tres llamadas
# 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.pdf
Autenticación

La 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.
Cabeceras
Authorization: Bearer dfk_…
X-DecaFirma-User: María Pérez
X-DecaFirma-Client: Odoo
Sin clave · 401
{
  "error": {
    "code": "missing_api_key",
    "message": "Missing API key: send the header «Authorization: Bearer dfk_…»."
  }
}
Errores

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.

Respuesta 400
{
  "error": {
    "code": "validation_error",
    "message": "The request is not valid.",
    "details": ["transportDate must be YYYY-MM-DD."]
  }
}
  • missing_api_key401Falta la cabecera Authorization.
  • invalid_api_key401La clave no existe o está revocada.
  • rate_limited429Más de 120 peticiones en un minuto con la misma clave.
  • plan_not_allowed403El plan de la empresa no incluye la API (en el plan gratis no hay).
  • addon_required403Falta el extra de conexión con tu programa en la cuenta.
  • key_owner_forbidden403Quien creó la clave ya no puede emitir en la empresa. Crea otra desde Configuración.
  • not_found404El envío, documento, ruta o papel no existe o no es de tu empresa.
  • validation_error400La petición no es válida. details trae la lista de mensajes, campo a campo (también un campo que no conocemos).
  • invalid_series400El seriesId no es una línea de numeración de albaranes viva de tu empresa.
  • payload_too_large413El cuerpo pasa del límite: 5 MB por PDF y 40 MB entre todos los de una llamada; en /sync, 500 elementos.
  • quota_exceeded429Sin lecturas disponibles en la cuenta (/parse).
  • service_unavailable503El servicio no está disponible ahora mismo. Reintenta más tarde.
  • upstream_error502Ha fallado un servicio del que dependemos. Reintenta en un momento.
  • not_a_shipment422El texto de /parse no encarga ningún porte.
  • extraction_failed422No se ha podido sacar nada del texto de /parse.
  • invalid_webhook_url400La dirección del webhook no es una URL https válida.
  • email_failed4xxNo se ha podido mandar el correo de /documents/:id/send. El motivo, en details.reason.
  • unprocessable422Una 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.
Errores

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).

Respuesta 422
{
  "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"
    }
  }
}
Varios problemas
{
  "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. fields con los nombres del cuerpo de /shipments (plate, shipperTaxId…). En una ruta, stop es la parada (1 la primera de las que mandas; sin él, falta en todas: la parte común) o position, el sitio del envío en la ruta.
  • invalid_tax_idfield, value, problem: incomplete | invalid, position?Un NIF que no cuadra: field es shipperTaxId o carrierTaxId.
  • transport_date_expiredfield, value (YYYY-MM-DD), daysLa fecha del transporte es de hace más de days dí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 en numbers).
  • 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, transportDate o plate que 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 fromDocumentId no se puede convertir en ruta (anulado o ya es una ruta).
  • deca_already_in_routedocumentId, routeIdEl envío del DeCA de fromDocumentId ya 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:…). En company.skipped de /sync, el correo de la empresa que no se ha guardado.
  • invalid_platefield, valueEn skipped de /sync: la matrícula no parece una matrícula.
  • invalid_formatfield, value, textEn skipped de /sync: el formato de numeración no vale (text, el porqué, en castellano).
  • format_takenformat, seriesEn skipped de /sync: otra serie ya usa ese formato.
  • duplicate_external_idsin datosEn skipped de /sync: el mismo externalId dos veces en la llamada; vale el primero.
  • unknown_referencefield, valueEn skipped de /sync: driverExternalId o employerExternalId no es nadie (vivo) de la llamada ni de DecaFirma; el de employerExternalId tiene que ser un carrier.
  • plate_takenplateEn skipped de /sync: la matrícula ya es de otro vehículo con otro externalId.
  • tax_id_takentaxId, takenBy?En skipped de /sync: el NIF ya es de otra persona viva del equipo (una persona, una ficha); takenBy, su externalId.
  • tax_id_lockedvalue, currentEn company.skipped de /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 skipped de /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: text trae el texto de la aplicación, en castellano. No debería salir; si te sale, dínoslo.
Recurso

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.

Objeto

Shipment

Lo que devuelven 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, delivered o cancelled.
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 noteKind SERVICE).
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.
Objeto

Document

Cada DeCA o albarán, en su última versión. Una corrección saca versión nueva con el mismo QR.

Campos

id, versionstring, integer
El id del documento y su versión.
typeenum
deca o delivery_note.
referencestring
La referencia del documento (DECA-…, el número del albarán).
statusenum
active o voided.
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, weightKg pasa 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) o SERVICE (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 noteKind SERVICE lo 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[].returnMinutes en la sincronización), y si no hay, 30.
freightTermsenum
Portes: PREPAID (pagados) o COLLECT (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, both o none (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" } (con details.reason y details.document, el que no salió): lo corriges y llamas a /shipments/:id/issue.
Petición
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"
  }'
Respuesta 201 Created
{
  "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.
Petición
curl https://decafirma.com/api/v1/shipments/cmu… \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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.

Petición
curl -X PATCH https://decafirma.com/api/v1/shipments/cmu… \
  -H "Authorization: Bearer dfk_…" \
  -H "Content-Type: application/json" \
  -d '{"weightKg": 9350, "packages": 13}'
Respuesta 200
{
  "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
deca o delivery_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.

Petición
curl -X POST https://decafirma.com/api/v1/shipments/cmu…/issue \
  -H "Authorization: Bearer dfk_…" \
  -H "Content-Type: application/json" \
  -d '{"type": "deca"}'
Respuesta 201 Created
{
  "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.

Petición
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"}'
Respuesta 200
{
  "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": []
}
Guía

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.

Petición
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)\"}"
Recurso

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
deca o delivery_note. Sin él, los dos.
limitinteger
Cuántos como mucho, de 1 a 500. Por defecto, 100.
Petición
curl "https://decafirma.com/api/v1/documents?since=2026-10-07T00:00:00Z&limit=100" \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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.
Petición
curl https://decafirma.com/api/v1/documents/cmv…/pdf \
  -H "Authorization: Bearer dfk_…" \
  -o DECA.pdf
Respuesta 200
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.
Petición
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"}'
Respuesta 200
{
  "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).
Petición
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}'
Respuesta 200
{
  "sent": true,
  "to": "paco@transportes.es",
  "reference": "DECA-…"
}
Recurso

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.

Objeto

Route

Lo que devuelven las llamadas de rutas.

Campos

idstring
El id de la ruta.
referencestring
Tu referencia.
decaobject | null
El DeCA de la ruta: { id, reference, version, isDemo, status, voidReason?, publicUrl, pdfUrl, updatedAt }, con los mismos significados que en Document.
stopsShipment[]
Cada parada, en orden, como un Shipment con su route.stop.

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) o pickup (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.
Petició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
  }'
Respuesta 201 Created
{
  "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.
Petición
curl https://decafirma.com/api/v1/routes/cmr… \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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.
Petición
curl -X POST https://decafirma.com/api/v1/routes/cmr…/stops \
  -H "Authorization: Bearer dfk_…" \
  -H "Content-Type: application/json" \
  -d '{"stops": [ … ], "deliveryNotes": true}'
Respuesta 201 Created
{
  "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.
Petición
curl -X DELETE https://decafirma.com/api/v1/routes/cmr…/stops/cmu… \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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.

Petición
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…" }
  }'
Respuesta 200
{
  "created": ["05402", "05403"],
  "skipped": [
    {
      "shipmentId": "cmu…3",
      "reason": "missing_delivery_note_number",
      "message": "Missing its delivery note number in «numbers»."
    }
  ]
}
Recurso

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.

Petición
curl https://decafirma.com/api/v1/series \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "data": [
    { "id": "cms…", "name": "Albaranes Onda", "nextNumber": "ON-2026-0018" }
  ],
  "defaultNextNumber": "ALB-2026-0043",
  "manualNumberingDefault": false,
  "nextManualNumber": "05404"
}
Recurso

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.

Petición
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"
  }'
Respuesta 200
{
  "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.

Petición
curl https://decafirma.com/api/v1/cmr/cmv… \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{ "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.

Petición
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" }'
Respuesta 200
{ "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.

Petición
curl https://decafirma.com/api/v1/cmr/cmv…/pdf \
  -H "Authorization: Bearer dfk_…" -o cmr.pdf
Respuesta 200
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.

Petición
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" }'
Respuesta 200
{ "id": "cmv…", "version": 2, "status": "active" }
Recurso

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.

Objeto

Attachment

Un papel del porte.

Campos

idstring
El id del papel.
typeenum | null
delivery_note, consignment_note, weighbridge_ticket, ticket, other o 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.
Petición
curl "https://decafirma.com/api/v1/attachments?since=2026-10-07T00:00:00Z&limit=100" \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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.
Petición
curl https://decafirma.com/api/v1/attachments/cmp…/file \
  -H "Authorization: Bearer dfk_…" \
  -o papel.jpg
Respuesta 200
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.
Petición
curl -X POST https://decafirma.com/api/v1/attachments/cmp…/invoiced \
  -H "Authorization: Bearer dfk_…" \
  -H "Content-Type: application/json" \
  -d '{"invoiced": true}'
Respuesta 200
{
  "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"
}
Recurso

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.

Petición
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"}'
Respuesta 200
{
  "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 }
  }
}
Recurso

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. taxId solo se comprueba: el NIF de la empresa no cambia nunca.
settingsobject
manualDeliveryNoteNumber, askForAttachments, hourlyServices (true/false) y deliveryNoteFormat, el formato de la serie por defecto (ALB[YY]%%%%%).
series[]array
externalId, name, format, default, nextNumber (solo sube el contador, nunca repite números) y archived.
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), invite y archived.
vehicles[]array
externalId, plate, type, description, driverExternalId (quien lo lleva; el camión de un autónomo, con él) y archived.

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.

Petición
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" }
    ]
  }'
Respuesta 200
{
  "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.

Petición
curl https://decafirma.com/api/v1/sync   -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "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 }]
}
Recurso

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/pdf da 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, con message.

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 la signature del albarán al firmarse).
attachmentAttachment
Solo en attachment.received: el papel.
messagestring
Solo en test.

Un aviso que no llega no se reintenta

Para no perder nada, ponte al día de vez en cuando con GET /documents?since=… (y GET /attachments para los papeles), guardando until.
El aviso
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.

Node.js
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)
}
Python
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.
Petición
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"}'
Respuesta 201 Created
{
  "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.

Petición
curl https://decafirma.com/api/v1/webhook \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "url": "https://tu-programa.es/decafirma",
  "lastDeliveryAt": "2026-10-07T10:42:00.000Z",
  "lastError": null
}

Quitarlo

DELETE/webhook

Dejan de salir avisos.

Petición
curl -X DELETE https://decafirma.com/api/v1/webhook \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{ "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.

Petición
curl -X POST https://decafirma.com/api/v1/webhook/test \
  -H "Authorization: Bearer dfk_…"
Respuesta 200
{
  "ok": true,
  "detail": "HTTP 200"
}