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ódigo | Cuándo |
|---|---|
| 401 | Sin token, token falso, revocado o de otra empresa |
| 403 | Empresa suspendida |
| 404 | Subdominio sin empresa, ruta que no existe o solicitud de otra integración |
| 422 | Datos 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" }
]
}
| Campo | Reglas |
|---|---|
asunto | Obligatorio, hasta 190 caracteres |
caduca_at | Opcional: entre 1 hora y 1 año. Por defecto, 30 días |
firma_manuscrita | no (por defecto), opcional u obligatoria |
orden_de_firma | paralelo (por defecto: todos a la vez) o secuencial: en el orden de firmantes, cada uno recibe la invitación cuando firma el anterior |
documentos | De 1 a 10 PDF en base64, de hasta 20 MB cada uno |
firmantes | De 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 }
]
xey: 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 escribe09/11/2026),casilla(trueescribe una X), o uno de los del firmante:firma,iniciales,fecha_de_firma,nombreonif.- Los del firmante llevan
firmante(su número en el orden defirmantes) y no llevanvalor: Firmalum los rellena al sellar (especificación 0012).firmapinta su firma dibujada o, si no dibujó, «Firmado electrónicamente por … el …» (y usaanchoyalto);inicialesescribe 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);nombreynif, los suyos (sin NIF queda en blanco). Sinfirmantees un 422. - Hasta 50 campos por documento. Una página que no existe es un 422.
- La
claveno 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, luegoplantillasy despuésdocumentos, cada uno en su orden. Conorden(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.