Para integradores

Documentación de la API

Cada empresa tiene su API en su propio subdominio: https://<empresa>.firmalum.com/api/v1. Aquí está la guía completa para leerla antes de tener cuenta.

Versión en producción cd1e409, del 11 de noviembre de 2026.

Dirección y autenticación

  • Cada empresa tiene su API en su subdominio: https://<empresa>.firmalum.com/api/v1 (testing: https://<empresa>.test.firmalum.com/api/v1).
  • Token Bearer de una integración, creado por el administrador de la empresa en Integraciones. Se enseña una vez; en la base solo queda su hash. Revocar la integración lo invalida al momento.
  • La empresa la da el dominio: un token de otra empresa recibe 401. Y una integración solo ve las solicitudes que ha creado ella (404 para las demás).
Authorization: Bearer 3|kB0x…
Accept: application/json

Errores

CódigoCuándo
401Sin token, token falso, revocado o de otra empresa
403Empresa suspendida
404Subdominio sin empresa, ruta que no existe o solicitud de otra integración
422Datos no válidos (errors por campo) o cancelar una solicitud que ya no admite firmas

Endpoints

GET /empresa

Comprueba que el token y la dirección son correctos: devuelve empresa e integracion.

POST /solicitudes

Crea una solicitud y manda la invitación por correo a cada firmante.

Cabecera opcional Idempotency-Key: si se repite la misma clave, se devuelve la solicitud ya creada (200 en vez de 201) y no se manda nada otra vez.

{
  "asunto": "Contrato de suministro",
  "mensaje": "Revísalo y fírmalo antes del viernes.",
  "referencia_externa": "CTR-2026-0042",
  "caduca_at": "2026-12-01T23:59:00+01:00",
  "firma_manuscrita": "opcional",
  "orden_de_firma": "secuencial",
  "documentos": [
    { "nombre": "contrato.pdf", "contenido_base64": "JVBERi0xLjcK…" }
  ],
  "firmantes": [
    { "nombre": "Lucía Pérez", "nif": "12345678Z", "email": "lucia@cliente.es", "telefono": "612345678", "metodo_otp": "sms" }
  ]
}
CampoReglas
asuntoObligatorio, hasta 190 caracteres
caduca_atOpcional: entre 1 hora y 1 año. Por defecto, 30 días
firma_manuscritano (por defecto), opcional u obligatoria
orden_de_firmaparalelo (por defecto: todos a la vez) o secuencial: en el orden de firmantes, cada uno recibe la invitación cuando firma el anterior
documentosDe 1 a 10 PDF en base64, de hasta 20 MB cada uno
firmantesDe 1 a 10. canal, por dónde recibe el enlace: email (por defecto), sms o ambos. metodo_otp, por dónde recibe el código: sms (por defecto) o email. El correo es obligatorio si alguno de los dos va por correo, y el móvil válido si alguno va por SMS

Un firmante solo con móvil (especificación 0016) lleva "canal": "sms" y no necesita email. Recibe el enlace por SMS, con el remitente de la marca de la empresa, y, al sellarse la solicitud, otro SMS con su enlace, que durante 30 días le deja descargar los documentos sellados y el dossier:

{ "nombre": "Lucía Pérez", "telefono": "612345678", "canal": "sms" }

Dos firmantes distintos no pueden recibir el código en el mismo sitio (mismo móvil con metodo_otp: sms, o mismo correo con email): da 422. Sí pueden compartir correo si cada uno recibe el código en su móvil. La misma persona dos veces (mismo NIF o, sin NIF, mismo nombre) se admite.

Cada documento puede llevar campos (especificación 0005): valores que Firmalum escribe en el PDF antes de enviarlo. Lo que se firma, y el hash_sha256 de la evidencia, es el PDF ya rellenado.

"campos": [
  { "clave": "titular", "tipo": "texto", "valor": "Lucía Pérez", "pagina": 1, "x": 72, "y": 140, "tamano": 10 },
  { "clave": "importe", "tipo": "importe", "valor": 1234.5, "pagina": 1, "x": 400, "y": 300 },
  { "clave": "firma_cliente", "tipo": "firma", "pagina": 2, "x": 72, "y": 650, "firmante": 1, "ancho": 160, "alto": 50 }
]
  • x e y: puntos PDF (1/72 de pulgada) desde la esquina superior izquierda de la página tal como se ve (con su giro y su recorte).
  • tipo: texto, numero, importe (1.234,50 €), fecha (AAAA-MM-DD, se escribe 09/11/2026), casilla (true escribe una X), o uno de los del firmante: firma, iniciales, fecha_de_firma, nombre o nif.
  • Los del firmante llevan firmante (su número en el orden de firmantes) y no llevan valor: Firmalum los rellena al sellar (especificación 0012). firma pinta su firma dibujada o, si no dibujó, «Firmado electrónicamente por … el …» (y usa ancho y alto); iniciales escribe las de su nombre («LP»); fecha_de_firma, el día y la hora en que firmó (hora de Madrid, 11/11/2026 09:15); nombre y nif, los suyos (sin NIF queda en blanco). Sin firmante es un 422.
  • Hasta 50 campos por documento. Una página que no existe es un 422.
  • La clave no se repite en el mismo documento (sin distinguir mayúsculas): una repetida es un 422. Varias firmas del mismo firmante, con claves distintas (firma_1, firma_1_2).

Respuesta 201 con la solicitud (ver abajo). Cada firmante trae su url_firma, por si el integrador quiere mandar el enlace por su cuenta.

GET /plantillas

Las plantillas activas de la empresa (PLANTILLAS.md) y qué pide cada una: variables con tipo, etiqueta, obligatoria y si (las combinaciones de condiciones con las que se pide; vacía = siempre), condiciones y globales (nombre y descripcion de las variables que Firmalum rellena solo, como hoy.larga o firmante.1.nombre: no hay que mandarlas, y si se mandan se ignoran; especificación 0009) y firmas (marca y descripcion de las marcas {{firma.N}}, {{iniciales.N}} y {{fecha_firma.N}}: dónde se estampa lo de cada firmante al sellar, sin coordenadas; especificación 0013).

Solicitud desde plantilla

En POST /solicitudes, en lugar de (o además de) documentos:

"plantilla": {
  "id": 3,
  "variables": { "titular": "Energía Úbeda SL", "ES_EMPRESA": true, "cif": "B12345678", "importe": 1234.5 },
  "campos": [ { "clave": "firma", "tipo": "firma", "pagina": 2, "x": 72, "y": 650, "firmante": 1 } ]
}

Firmalum genera el PDF (va el primero de los documentos) y responde 201 como siempre. Solo se exigen las variables que se ven con las condiciones dadas; los errores llegan como variables.<nombre>.

Varias plantillas en una solicitud (especificación 0006)

plantillas es una lista de lo mismo, y se puede mezclar con documentos (PDF). El firmante los lee todos y firma una vez:

"plantillas": [
  { "id": 3, "variables": { "titular": "Ana Gómez", "importe": 45 } },
  { "id": 5, "variables": { "titular": "Ana Gómez", "iban": "ES91 2100 0418 4502 0005 1332" } }
],
"documentos": [ { "nombre": "condiciones.pdf", "contenido_base64": "…" } ]
  • Orden: primero plantilla, luego plantillas y después documentos, cada uno en su orden. Con orden (1, 2, 3…) en cualquiera de ellos se intercalan: manda el número, y los que no lo llevan van detrás.
  • Variables: cada plantilla lleva las suyas, aunque se repitan.
  • Errores: plantillas.<n>.variables.<nombre>. Si falla una plantilla no se genera ninguna.
  • Máximo: 10 documentos entre PDF y plantillas.
  • Todas se convierten en una sola llamada a LibreOffice (ADR 0003).

GET /solicitudes

Lista paginada (25) de las solicitudes de la integración, de la más nueva a la más antigua. Filtros: ?estado= y ?referencia_externa=.

GET /documentos-firmados

Documentos firmados (especificación 0014): las solicitudes selladas de la integración, una por expediente (con todos sus documentos y su dossier), del sellado más reciente al más antiguo. Mismo formato que GET /solicitudes, sin url_firma. Filtros: ?q= (asunto, referencia, nombre de documento o firmante: nombre, NIF o correo), ?desde= y ?hasta= (AAAA-MM-DD, día de sellado en hora de Madrid) y ?carpeta= (nombre exacto).

Al crear la solicitud, carpeta (opcional) dice en qué carpeta de Documentos firmados se archivará: el nombre exacto de una carpeta que exista (las crea un administrador en el panel); si no existe, 422. Cada solicitud lleva carpeta (o null).

GET /solicitudes/{id}

{
  "id": "9b1f…",
  "estado": "abierta",
  "asunto": "Contrato de suministro",
  "referencia_externa": "CTR-2026-0042",
  "firma_manuscrita": "opcional",
  "caduca_at": "2026-12-01T22:59:00+00:00",
  "completada_at": null,
  "documentos": [{ "id": 1, "nombre": "contrato.pdf", "hash_sha256": "…", "tamano": 48213 }],
  "firmantes": [{ "id": 1, "nombre": "Lucía Pérez", "estado": "abierto", "firmado_at": null, "url_firma": "https://acme.firmalum.com/firmar/…" }]
}

turno_de es el id del firmante al que le toca en una solicitud secuencial (null en paralelo o si ya no admite firmas). Un firmante que abre su enlace antes de su turno ve que aún no le toca y no puede pedir el código.

Estados de la solicitud: enviada → abierta (algún firmante ha abierto el enlace) → firmada (han firmado todos). También rechazada (basta un firmante), caducada y cancelada. Estados de un firmante: pendiente, abierto, firmado y rechazado.

POST /solicitudes/{id}/cancelar

Solo si está enviada o abierta. Los enlaces dejan de servir.

GET /solicitudes/{id}/documentos/{documento}

Descarga el PDF: el sellado (PAdES, hash_sellado) si la solicitud ya está sellada; si no, o con ?original=1, el que se envió a firmar (hash_sha256, el de las evidencias).

GET /solicitudes/{id}/dossier

El dossier probatorio sellado (404 hasta que la solicitud esté sellada: sellada_at).

GET /solicitudes/{id}/evidencias

El registro de evidencias completo y si la cadena está íntegra (EVIDENCIAS.md).

Webhooks

Cada integración puede tener una dirección de avisos (Integraciones → Webhook). Al guardarla se genera un secreto whsec_… que se enseña una vez. Solo https (en local se puede permitir http con FIRMALUM_WEBHOOKS_EXIGIR_HTTPS=false).

Eventos: solicitud.enviada, solicitud.abierta, firmante.firmado, solicitud.firmada, solicitud.sellada (documentos sellados y dossier listos: es el momento de descargarlos), solicitud.rechazada, solicitud.caducada y solicitud.cancelada.

POST <tu dirección>
Content-Type: application/json
Firmalum-Evento: solicitud.firmada
Firmalum-Entrega: 4c1d…            (id único del aviso: úsalo para no procesarlo dos veces)
Firmalum-Firma: t=1762700000,v1=5f2a…

{"id":"4c1d…","evento":"solicitud.firmada","creado_at":"…","empresa":"acme","solicitud":{…}}

Cómo comprobar la firma: calcula HMAC-SHA256(secreto, "<t>.<cuerpo crudo>") en hexadecimal y compáralo en tiempo constante con v1. Rechaza los avisos con t de hace más de 5 minutos.

La solicitud del aviso no incluye url_firma. Si la respuesta no es 2xx, el aviso se reintenta hasta 10 veces durante unas 24 horas (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h, 12 h). El orden de llegada no está garantizado: usa GET /solicitudes/{id} si necesitas el estado actual.

Referencia

Cada endpoint con sus parámetros, el cuerpo que espera y lo que devuelve. Todas las rutas cuelgan de https://<empresa>.firmalum.com/api/v1 y piden el token. Descarga la especificación para generar un cliente: openapi.yaml (OpenAPI 3.1).

GET /empresa

Empresa e integración del token

Respuestas

  • 200 OK

    • empresa object

    • empresa.id string

    • empresa.nombre string

    • integracion object

    • integracion.id integer

    • integracion.nombre string

  • 401 Sin token

GET /plantillas

Plantillas activas y qué pide cada una

Ver la guía de plantillas. globales las rellena Firmalum; firmas dice dónde se estampa lo de cada firmante.

Respuestas

  • 200 Plantillas

    • datos lista de object

    • datos[].id integer

    • datos[].nombre string

    • datos[].descripcion string | null

    • datos[].version integer

    • datos[].variables lista de object

    • datos[].variables[].nombre string

    • datos[].variables[].etiqueta string

    • datos[].variables[].tipo una de: texto, numero, importe, fecha, casilla

    • datos[].variables[].obligatoria boolean

    • datos[].variables[].si lista de lista de string

      Combinaciones de condiciones con las que se pide (vacía: siempre); «!X» es X falsa.

    • datos[].condiciones lista de string

    • datos[].globales lista de object

    • datos[].globales[].nombre string

    • datos[].globales[].descripcion string

    • datos[].firmas lista de object

    • datos[].firmas[].marca string

    • datos[].firmas[].descripcion string

  • 401 Sin token

GET /documentos-firmados

Documentos firmados (expedientes sellados) de la integración

Especificación 0014: cada solicitud sellada, con sus documentos y su dossier. Sin url_firma.

Parámetros

  • q string · consulta

    Asunto, referencia, nombre de documento o firmante (nombre, NIF o correo).

  • desde string (date) · consulta

    Sellado desde este día (hora de Madrid).

  • hasta string (date) · consulta

  • carpeta string · consulta

    Nombre exacto de la carpeta.

  • page integer · consulta

    Mín. 1.

Respuestas

  • 200 Página de 25, del sellado más reciente al más antiguo

GET /solicitudes

Solicitudes de la integración

Parámetros

Respuestas

POST /solicitudes

Crear una solicitud de firma

Parámetros

  • Idempotency-Key string · cabecera

    Repetida, devuelve la solicitud ya creada (200) sin duplicar. Máx. 190 caracteres.

Cuerpo application/json

NuevaSolicitud

Respuestas

  • 200 Ya existía con esa Idempotency-Key → Solicitud

  • 201 Creada; las invitaciones ya se han enviado → Solicitud

  • 422 Datos no válidos

    • message string

    • errors object

GET /solicitudes/{id}

Una solicitud

Parámetros

  • id string (uuid) · ruta obligatorio

Respuestas

  • 200 OK → Solicitud

  • 404 No existe o es de otra integración

POST /solicitudes/{id}/cancelar

Cancelar (solo enviada o abierta)

Parámetros

  • id string (uuid) · ruta obligatorio

Respuestas

  • 200 Cancelada → Solicitud

  • 422 Datos no válidos

    • message string

    • errors object

GET /solicitudes/{id}/documentos/{documento}

Descargar un PDF

Parámetros

  • id string (uuid) · ruta obligatorio

  • documento integer · ruta obligatorio

Respuestas

  • 200 PDF application/pdf

GET /solicitudes/{id}/dossier

Descargar el dossier probatorio

Cuando la solicitud está sellada; antes, 404.

Parámetros

  • id string (uuid) · ruta obligatorio

Respuestas

  • 200 PDF application/pdf

  • 404 Todavía no hay dossier

GET /solicitudes/{id}/evidencias

Registro de evidencias y si está íntegro

Parámetros

  • id string (uuid) · ruta obligatorio

Respuestas

  • 200 OK

    • integra boolean

    • evidencias lista de object

    • evidencias[].secuencia integer

    • evidencias[].tipo string

    • evidencias[].firmante_id integer | null

    • evidencias[].datos object

    • evidencias[].ip string | null

    • evidencias[].user_agent string | null

    • evidencias[].registrada_at string

    • evidencias[].hash_anterior string | null

    • evidencias[].hash string

Esquemas

EstadoSolicitud

una de: enviada, abierta, firmada, rechazada, caducada, cancelada.

NuevaSolicitud

Hace falta al menos uno de documentos, plantilla o plantillas; entre todos, 10 documentos como mucho.

  • asunto string obligatorio

    Máx. 190 caracteres.

  • mensaje string | null

    Máx. 2000 caracteres.

  • referencia_externa string | null

    Máx. 190 caracteres.

  • carpeta string | null

    Carpeta de Documentos firmados donde se archivará, por su nombre exacto (la crea un administrador en el panel). Máx. 80 caracteres.

  • caduca_at string (date-time) | null

    Entre 1 hora y 1 año; por defecto 30 días.

  • firma_manuscrita una de: no, opcional, obligatoria

    Por defecto: no.

  • orden_de_firma una de: paralelo, secuencial

    secuencial: cada firmante recibe la invitación cuando firma el anterior. Por defecto: paralelo.

  • plantilla DesdePlantilla

  • plantillas lista de DesdePlantilla

    Varias plantillas en la misma solicitud (especificación 0006). Se generan en una sola conversión; errores como plantillas.<n>.variables.<nombre>. Mín. 1 elemento. Máx. 10 elementos.

  • documentos lista de object

    Mín. 1 elemento. Máx. 10 elementos.

  • documentos[].nombre string obligatorio

    Máx. 190 caracteres.

  • documentos[].contenido_base64 string obligatorio

    PDF en base64, hasta 20 MB.

  • documentos[].orden integer

    Posición entre todos los documentos (especificación 0006). Mín. 1.

  • documentos[].campos Campos

  • firmantes lista de object obligatorio

    Mín. 1 elemento. Máx. 10 elementos.

  • firmantes[].nombre string obligatorio

  • firmantes[].nif string | null

  • firmantes[].email string (email) | null

    Obligatorio con canal email o ambos, o con metodo_otp email.

  • firmantes[].telefono string | null

    Móvil válido obligatorio con canal sms o ambos, o con metodo_otp sms.

  • firmantes[].canal una de: email, sms, ambos

    Por dónde recibe el enlace de firma (especificación 0016). Sin correo, la copia firmada llega por SMS. Por defecto: email.

  • firmantes[].metodo_otp una de: sms, email

    Por defecto: sms.

DesdePlantilla

  • id integer obligatorio

  • variables object

    Valores de las variables y condiciones, por nombre (GET /plantillas dice cuáles). Las globales (con punto, como hoy.larga) las rellena Firmalum y se ignoran si se mandan.

  • orden integer

    Posición entre todos los documentos (especificación 0006). Mín. 1.

  • campos Campos

Campos

Se escriben en el PDF antes de enviarlo (especificación 0005). lista de object.

Solicitud

  • id string (uuid)

  • estado EstadoSolicitud

  • asunto string

  • mensaje string | null

  • referencia_externa string | null

  • firma_manuscrita una de: no, opcional, obligatoria

  • orden_de_firma una de: paralelo, secuencial

  • turno_de integer | null

    En secuencial, el firmante al que le toca.

  • sellada_at string (date-time) | null

  • carpeta string | null

    Carpeta de Documentos firmados.

  • hash_dossier string | null

  • caduca_at string (date-time)

  • completada_at string (date-time) | null

  • cancelada_at string (date-time) | null

  • creada_at string (date-time)

  • documentos lista de object

  • documentos[].id integer

  • documentos[].nombre string

  • documentos[].hash_sha256 string

  • documentos[].tamano integer

  • firmantes lista de object

  • firmantes[].id integer

  • firmantes[].nombre string

  • firmantes[].nif string | null

  • firmantes[].email string | null

  • firmantes[].telefono string | null

  • firmantes[].canal una de: email, sms, ambos

  • firmantes[].metodo_otp una de: sms, email

  • firmantes[].estado una de: pendiente, abierto, firmado, rechazado

  • firmantes[].abierto_at string (date-time) | null

  • firmantes[].firmado_at string (date-time) | null

  • firmantes[].rechazado_at string (date-time) | null

  • firmantes[].motivo_rechazo string | null

  • firmantes[].url_firma string

    Solo en la API, nunca en los webhooks.

¿Ya sois clientes?

En vuestro subdominio está esta misma documentación con vuestra dirección en los ejemplos, y la referencia interactiva de cada endpoint.

Es la primera parte de la dirección de tu panel: si entras en acme.firmalum.com, escribe acme. Solo minúsculas, cifras y guiones. Te llevamos a /docs/api de tu empresa.

¿Aún no tenéis empresa en Firmalum? Cuéntanos qué queréis integrar y te damos acceso a un entorno de pruebas.