# Firmame — documentación para desarrolladores
> Firmame es un servicio de firma electrónica. Esta es la documentación de su API, cuya URL base es https://app.firmame.com.
- Cada página se sirve también en markdown: añade `.md` a su URL, por ejemplo https://devs.firmame.com/docs/autenticacion.md
- Especificación OpenAPI: https://devs.firmame.com/openapi.yaml
- Índice de páginas: https://devs.firmame.com/llms.txt
- Versión en inglés de este archivo: https://devs.firmame.com/en/llms-full.txt
- Toda petición se autentica con una firma HMAC. Las reglas van más abajo; si no coinciden, la respuesta es `401` con cuerpo vacío.
Cada petición lleva `Authorization: signmage {webId}:{firma}`, donde `firma` es el Base64 de
`HMAC-SHA256(apiKey, cadenaFirmada)`.
Qué va dentro de `cadenaFirmada` depende de la petición:
| Petición | `cadenaFirmada` |
|---|---|
| `GET` | La URI completa **incluyendo el query string**, exactamente como se envía |
| `POST` con JSON | El cuerpo JSON, re-serializado compacto (sin espacios ni salto de línea final) |
| `POST` multipart | El SHA-256 en Base64 del archivo, que además se manda en el header `Content-SHA256` |
Dos detalles que conviene resolverlos desde el principio:
1. En `GET`, la URI se firma tal como se envía, así que **el orden y el encoding de los
parámetros importan**.
2. En `POST`, la comparación se hace contra tu JSON re-serializado en forma compacta. Manda
JSON compacto y canónico para que las firmas coincidan.
Una firma que no coincide devuelve `401` con el cuerpo vacío. Como la misma respuesta cubre un
web ID desconocido, una llave inactiva y una firma que no coincide, revisa las tres cosas al
diagnosticar.
# Conceptos (https://devs.firmame.com/docs/conceptos)
> El vocabulario de Firmame.
Antes de escribir código conviene fijar ocho términos.
## Documento [#documento]
El PDF que se firma. Al crearlo, Firmame le asigna un identificador único que vas a usar en todas las
llamadas posteriores.
```js
const { document_ticket } = await services.request({ /* … */ });
// más tarde, el mismo valor:
await services.docs({ ticket: document_ticket, action: 'delete' });
// y en el webhook que te llega:
// { "firmamex_id": "04337396-e929-43ca-9b8a-ec958a44079d", … }
```
## Sticker [#sticker]
Un recuadro de firma: **una persona, en un lugar, en una página**. Un documento sin stickers se guarda,
pero nadie puede firmarlo.
Cada sticker define quién firma (`data`), cómo se verifica esa persona (`authority`), en qué página va
(`page`) y en qué coordenadas (`rect`).
Hay dos formas de colocarlos: mandándolos en el arreglo `stickers` con coordenadas explícitas, o
[dibujándolos dentro del PDF](/docs/guias/recuadros-de-firma) para que Firmame los encuentre.
## Authority [#authority]
El método con el que el firmante comprueba que es quien dice ser: su certificado del SAT, una liga que
recibe por correo, un PIN por WhatsApp, un SMS.
Es la decisión de más peso de cada sticker: define **qué queda comprobado** de la persona que firmó, y por
lo tanto qué evidencia respalda esa firma después. No comprueban lo mismo un certificado del SAT y una
liga enviada por correo.
También determina quién manda la invitación. Con las autoridades **por liga**, Firmame la envía
automáticamente y solo el firmante tiene la URL que autoriza firmar a su nombre. Con `SAT`, `EMAIL` y
`SMS`, si no configuras un flujo, repartir la URL de firma es responsabilidad tuya.
El catálogo completo está en el [esquema `Authority`](/docs/guias/firmantes) de la
referencia.
## Verificación de identidad [#verificación-de-identidad]
Una capa adicional sobre la `authority`: además de verificar el canal, el firmante comprueba con una
identificación oficial y una **prueba de vida** —una selfie que confirma que es una persona real, no una
foto ni una pantalla— que es quien dice ser.
Se activa por sticker con `requireIdentityVerification`, y consume créditos adicionales de la cuenta. Ver
[Verificación de identidad](/docs/guias/verificacion-de-identidad).
## Flujo de firma [#flujo-de-firma]
La configuración de **cómo** se recolectan las firmas: en orden o en paralelo, cada cuánto se recuerda a
quien falta, cuándo expira, quién recibe copia sin firmar.
Sin flujo, tienes un documento firmable y tú decides cómo hacérselo llegar a cada persona. Con flujo,
Firmame manda las invitaciones, los recordatorios y avisa cuando se completa.
Se puede adjuntar al crear el documento o agregarse después. Ver [Flujos de firma](/docs/guias/flujos).
## Expediente [#expediente]
Un paquete de varios documentos que se firman juntos: una invitación, un orden de firma, una conclusión.
El ciclo es crear el expediente, agregarle documentos, y cerrarlo para arrancar el flujo. Ver
[Expedientes](/docs/guias/expedientes).
## Original y universal [#original-y-universal]
Dos representaciones firmadas **del mismo documento**, no dos documentos:
| | Qué es | Cuándo la quieres |
| ------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **original** | El PDF con las firmas tal como se aplicaron | Para mostrarlo o descargarlo en el momento |
| **universal** | Esas mismas firmas envueltas en un sobre de validación a largo plazo | Para archivar, porque sigue siendo verificable cuando los certificados ya expiraron |
Ambas se descargan con [`GET /api/document/{type}/{ticket}`](/docs/api/descargas/download-document), y
cada una tiene su propio webhook: `original_signed` y `universal_signed`.
## NOM-151 [#nom-151]
La norma mexicana de constancia de conservación. Un sello NOM-151 certifica ante un tercero que un
documento existía, con ese contenido exacto, en un momento dado.
Firmame lo aplica automáticamente a los documentos que firma, y también lo expone por separado para
sellar archivos o hashes que ni siquiera pasaron por la plataforma. Ver
[NOM-151](/docs/guias/nom151).
# Introducción (https://devs.firmame.com/docs)
> Integra la firma electrónica de Firmame en tu aplicación.
Firmame es un servicio de firma electrónica. Su API te permite crear documentos, colocar recuadros de
firma, invitar a los firmantes, seguir el avance y descargar el documento firmado con su evidencia.
La mayoría de las integraciones se resuelven con tres llamadas y un webhook.
## El flujo de integración [#el-flujo-de-integración]
1. **Crea el documento.** Envías el PDF —por URL, base64 o desde un template— junto con los *stickers*:
los recuadros donde cada persona debe firmar.
2. **Invita a los firmantes.** Con un *flujo de firma* controlas si es en orden o en paralelo, los
recordatorios y la fecha de expiración. Si prefieres repartir tú las ligas, puedes omitirlo.
3. **Escucha los webhooks.** Firmame notifica cada firma y el cierre del documento a la URL que registres.
4. **Descarga el resultado.** [El documento firmado](/docs/api/descargas/download-document) y su
historial de evidencia, en [JSON](/docs/api/consultas/get-document-history) o en
[PDF](/docs/api/consultas/get-document-history-pdf).
## Autenticación en una línea [#autenticación-en-una-línea]
Cada solicitud lleva un HMAC-SHA256 firmado con tu API key:
```
Authorization: signmage TU_WEB_ID:
```
Los SDKs la construyen por ti. Si llamas a la API directamente, la
[guía de autenticación](/docs/autenticacion) explica qué entra en `cadenaFirmada` según el tipo de
petición.
## Por dónde empezar [#por-dónde-empezar]
# Inicio rápido con IA (https://devs.firmame.com/docs/inicio-rapido-ai)
> Dale esta documentación a tu asistente y pídele la integración. Prompt de arranque y URLs legibles por máquina.
Toda esta documentación está publicada también en formatos que lee una máquina: markdown crudo por página,
un archivo con el portal completo, y la especificación OpenAPI. Si integras con ayuda de un asistente, dale
eso como contexto en lugar de dejarlo adivinar.
## El prompt de arranque [#el-prompt-de-arranque]
Cópialo, pégalo en tu asistente y describe tu caso en la última línea.
Lleva las tres cosas que necesita saber antes de escribir la primera línea: dónde está la documentación, el
flujo de una integración típica, y **cómo se firma cada petición**. Esa última parte es la que conviene no
dejar al azar: una firma que no coincide devuelve `401` con cuerpo vacío, sin nada en la respuesta de dónde
agarrarse. Si quieres el detalle, está en [Errores 401](/docs/autenticacion/errores-401).
## Las URLs legibles por máquina [#las-urls-legibles-por-máquina]
Todas existen en los dos idiomas: el inglés vive bajo `/en` — `/en/llms-full.txt`, `/en/openapi.yaml`,
`/en/docs/autenticacion.md`.
## Cargar la documentación como contexto [#cargar-la-documentación-como-contexto]
Guarda el archivo en el repositorio donde estás trabajando y menciónalo en tu petición:
```bash
curl -o docs/firmame.md https://devs.firmame.com/llms-full.txt
```
Así queda disponible en cada sesión sin volver a descargarlo. También puedes pegar la URL en el chat y
pedirle que la lea.
Añade `https://devs.firmame.com/llms-full.txt` como documentación indexada en los ajustes, y después
referénciala con `@` en tus peticiones. La alternativa es la misma que en cualquier repositorio: guardar el
archivo dentro del proyecto.
Descarga `/llms-full.txt` y pégalo, o pasa la URL si tu asistente puede navegar. Si el archivo completo no
cabe en la ventana de contexto, empieza por `/llms.txt` y baja solo las páginas que haga falta con `.md`.
## Dos cosas que vale la pena revisar del código generado [#dos-cosas-que-vale-la-pena-revisar-del-código-generado]
**Que la cadena firmada se arme una sola vez.** En `GET` se firma la URI completa tal como se envía, y en
`POST` con JSON se firma el cuerpo compacto. Si el código construye la URL o serializa el JSON dos veces —una
para firmar y otra para enviar— cualquier diferencia mínima entre las dos versiones produce un `401`.
**Que los endpoints y los campos existan.** La [Referencia de API](/docs/api) es la lista completa: 35
operaciones y 12 eventos de webhook. Si el código llama a algo que no está ahí, viene de una suposición.
# SDKs (https://devs.firmame.com/docs/sdks)
> Instala o descarga el SDK de tu lenguaje, como firmame.ts para TypeScript. Se encargan de firmar cada petición por ti.
Los SDKs construyen el HMAC de cada petición y te dejan trabajar con objetos en vez de con cadenas
firmadas.
| Lenguaje | Cómo se obtiene |
| ---------- | ----------------------------------------------------------------------------------- |
| Node | [firmamex.js](https://assets.firmame.com/sdks/javascript/firmamex.js) |
| TypeScript | [firmame.ts](https://www.npmjs.com/package/firmame.ts) — `npm install firmame.ts` |
| PHP | [firmamex.php](https://assets.firmame.com/sdks/php/firmamex.php) |
| Python | [firmamex.py](https://assets.firmame.com/sdks/python/firmamex.py) |
| .NET | [SignmageSDKCore.dll](https://assets.firmame.com/sdks/net/SignmageSDKCore.dll) |
| Java 8 | [firmamex-services.zip](https://assets.firmame.com/sdks/java/firmamex-services.zip) |
`firmame.ts` requiere Node 20 o superior.
## Inicializar [#inicializar]
Todos reciben lo mismo: tu Web ID y tu API key. Cárgalos desde variables de ambiente, no escritos en el
código.
```js
const FirmamexServices = require('./firmamex');
const services = FirmamexServices(
process.env.FIRMAME_WEB_ID,
process.env.FIRMAME_API_KEY,
);
```
```ts
import { Firmame } from 'firmame.ts';
const firmame = new Firmame({
webId: process.env.FIRMAME_WEB_ID,
apiKey: process.env.FIRMAME_API_KEY,
});
```
```python
import os
from firmamex import SignmageServices
services = SignmageServices(
os.environ['FIRMAME_WEB_ID'],
os.environ['FIRMAME_API_KEY'],
)
```
```php
require_once 'firmamex.php';
$services = new FirmamexServices(
getenv('FIRMAME_WEB_ID'),
getenv('FIRMAME_API_KEY')
);
```
```java
SignmageServices services = new SignmageServices(
System.getenv("FIRMAME_WEB_ID"),
System.getenv("FIRMAME_API_KEY")
);
```
A partir de ahí, cada endpoint de la [referencia](/docs/api/documentos/create-document) tiene su método. Por ejemplo, crear un
documento:
```js
const response = await services.request({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
stickers: [
{
authority: 'EMAIL_LINK',
data: 'firmante@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
],
});
```
## Si tu lenguaje no está en la lista [#si-tu-lenguaje-no-está-en-la-lista]
La API es HTTP con JSON, así que se puede usar desde cualquier parte. Lo único que hay que implementar es
el encabezado de autenticación,
[Credenciales y firma](/docs/autenticacion) lo explica con un ejemplo completo.
# Zapier (https://devs.firmame.com/docs/zapier)
> Conecta Firmame con miles de apps sin escribir código, con triggers y acciones listos para usar.
Firmame tiene una integración publicada en Zapier: conecta tus documentos y firmas con miles de otras
apps sin escribir código, en
[zapier.com/apps/firmamex/integrations](https://zapier.com/apps/firmamex/integrations).
## Conectar tu cuenta [#conectar-tu-cuenta]
Al agregar la app de Firmame a un Zap, Zapier te pide tu **Web ID** y tu **API key** — las mismas
credenciales que usas para firmar peticiones a la API. Ver [Credenciales](/docs/autenticacion) si
todavía no las tienes a la mano.
## Triggers [#triggers]
Arrancan un Zap cuando algo pasa en Firmame. Los cinco son **instantáneos**: por debajo usan el mismo
[esquema de suscripciones por evento](/docs/webhooks/suscripciones) (beta) que documenta la sección de
Webhooks — Zapier registra la suscripción por ti, sin que tengas que llamar a `addWebhook` a mano.
| Trigger | Cuándo se dispara |
| -------------------------------- | --------------------------------------------- |
| **Document Completed** | El documento quedó firmado por todos |
| **Document Signed - Original** | Alguien firmó el documento |
| **Document Signed - Universal** | La versión universal de una firma quedó lista |
| **Document Rejected** | Un firmante rechazó el documento |
| **Document Stamped With NOM151** | El documento recibió un sello NOM-151 |
Corresponden al mismo [catálogo de eventos](/docs/api/webhooks-y-cuenta/document-completed) que
documenta la sección de Webhooks.
## Acciones [#acciones]
Ejecutan algo en Firmame desde un Zap.
| Acción | Qué hace | Equivalente en la API |
| -------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| **Upload Document With Signers** | Sube un documento (Base64 o URL) y opcionalmente le agrega firmantes | [Crear y enviar un documento](/docs/guias/crear-documento) |
| **Configure Workflow** | Configura el flujo de firma de un documento | [Flujos de firma](/docs/guias/flujos) |
| **Download Document (Base64)** | Descarga la versión original o universal de un documento, en Base64 | [Descargar un documento firmado](/docs/api/descargas/download-document) |
| **Upload Template** | Sube un template en HTML, PDFMake, PDF o DOCX | [Templates](/docs/guias/templates) |
| **Fill Template** | Llena un template con datos dinámicos y devuelve el resultado | [Templates](/docs/guias/templates) |
## Referencia [#referencia]
# Referencia de API (https://devs.firmame.com/docs/api)
{/* Generated from openapi/firmame.yaml by `npm run openapi`. Do not edit — your changes will be overwritten. */}
# Errores 401 (https://devs.firmame.com/docs/autenticacion/errores-401)
> Cómo diagnosticar una petición que devuelve 401, paso por paso.
Un `401` llega con el **cuerpo vacío** y cubre tres situaciones distintas: el Web ID no se reconoce, la
llave no está activa, o la firma no coincide.
## 1. Descarta lo simple [#1-descarta-lo-simple]
Antes de revisar la firma, confirma que el encabezado llegó completo y bien formado:
* Empieza con `signmage` seguido de un espacio.
* Entre el Web ID y la firma va un `:`, sin espacios alrededor.
* La firma va en Base64, no en hexadecimal.
```
Authorization: signmage TU_WEB_ID:PWJhc2U2NCBkZWwgSE1BQz4=
```
Si el Web ID o la llave no son los que te asignaron, o la llave ya no está activa, escribe a
**[soporte@firmamex.com](mailto:soporte@firmamex.com)**
## 2. En `GET`, la URI se firma tal cual [#2-en-get-la-uri-se-firma-tal-cual]
La cadena firmada es la ruta **con su query string**, exactamente como sale en la petición. Eso significa
que el orden de los parámetros y su codificación forman parte de la firma.
```js
// Construye la ruta una vez y usa esa misma cadena para firmar y para pedir.
const path = '/api/document?from=1640995200000&to=1641945600000';
const auth = authorization(path);
await fetch(`https://app.firmame.com${path}`, { headers: { Authorization: auth } });
```
Lo que suele descuadrar:
* Armar la URL con un objeto y dejar que la librería reordene los parámetros.
* Firmar la ruta sin query string y luego enviarla con parámetros.
* Codificar un valor al firmar y no al enviar, o al revés.
## 3. En `POST` con JSON, serializa una sola vez [#3-en-post-con-json-serializa-una-sola-vez]
La comparación se hace contra tu JSON **compacto**: sin espacios ni saltos de línea. Serializa una vez,
firma esa cadena y manda exactamente esos bytes.
```js
const body = JSON.stringify(payload); // una sola serialización
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authorization(body) },
body, // los mismos bytes que se firmaron
});
```
Lo que suele descuadrar:
* Firmar `payload` y dejar que el cliente HTTP serialice por su cuenta al enviar. Dos serializaciones
distintas producen dos cadenas distintas.
* Usar un formateador con indentación al firmar.
* Un cliente que agrega un salto de línea final al cuerpo.
Nota sobre los números: `1.0` y `1` son el mismo valor en JSON pero no la misma cadena. Si tu payload
lleva decimales, revisa que sobrevivan igual a la serialización que firmas y a la que envías.
## 4. En `POST` multipart, se firma el hash del archivo [#4-en-post-multipart-se-firma-el-hash-del-archivo]
La cadena firmada es el SHA-256 del archivo en Base64, y ese mismo valor va en el encabezado
`Content-SHA256`. El archivo viaja en el campo `file`.
```js
const hash = createHash('sha256').update(fileBytes).digest('base64');
const headers = {
Authorization: authorization(hash),
'Content-SHA256': hash,
};
```
## 5. Si aún falla, compara la cadena [#5-si-aún-falla-compara-la-cadena]
Cuando los cuatro puntos anteriores están bien y el `401` sigue, imprime la cadena que estás firmando y
revísala carácter por carácter contra lo que realmente sale por la red.
Los [SDKs](/docs/sdks) construyen la firma por ti en los cuatro casos. Si tu lenguaje está en la lista,
es la vía más directa.
# Credenciales y firma (https://devs.firmame.com/docs/autenticacion)
> Cómo obtener tus credenciales y cómo se arma el encabezado de autenticación de cada petición.
## Credenciales [#credenciales]
Tu cuenta tiene un par de credenciales de desarrollador. Si todavía no las tienes a la mano, debes poder generarlas dando click en tu usuario en el portal de Administración de usuarios de la cuenta. Si no tienes acceso a este portal, solicita las credenciales a tu administrador de cuetna
**Si no tienes cuenta, puedes obtener credenciales de prueba gratuitas**. Sólo envía un correo a [contacto@firmamex.com](mailto:contacto@firmamex.com).
| | Qué es | Cómo tratarla |
| ----------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
| **Web ID** | Tu identificador público | Puede ir en configuración, en el repositorio, donde sea |
| **API key** | El secreto con el que se firma cada petición | Trátala como una contraseña: autoriza a solicitar firmas a nombre de tu cuenta |
Carga las dos desde variables de ambiente o desde tu gestor de secretos, nunca escritas en el código:
```js
const services = FirmamexServices(
process.env.FIRMAME_WEB_ID,
process.env.FIRMAME_API_KEY,
);
```
## Con un SDK [#con-un-sdk]
Es el camino corto: le pasas las credenciales al SDK y él arma la firma de cada petición.
Hay SDK para [Node, TypeScript, PHP, Python, .NET y Java](/docs/sdks). Si tu lenguaje no está en la lista puedes llamar a la API directamente:
## Directo contra la API [#directo-contra-la-api]
Cada petición lleva un encabezado `Authorization` con esta forma:
B["TU_WEB_ID"]
B --> C[":"]
C --> D["base64 del HMAC-SHA256"]
style A fill:#0b66c9,color:#fff
style B fill:#0b66c9,color:#fff
style C fill:#64748b,color:#fff
style D fill:#0b66c9,color:#fff"
/>
```
Authorization: signmage TU_WEB_ID:PWJhc2U2NCBkZWwgSE1BQz4=
Content-Type: application/json
```
La firma es `base64(HMAC-SHA256(TU_API_KEY, cadenaFirmada))`. Lo único que cambia entre peticiones es qué
va dentro de `cadenaFirmada`:
| Petición | `cadenaFirmada` |
| ---------------- | ----------------------------------------------------------------------------------- |
| `GET` | La URI completa **incluyendo el query string**, exactamente como se envía |
| `POST` con JSON | El cuerpo JSON, re-serializado compacto (sin espacios ni salto de línea final) |
| `POST` multipart | El SHA-256 en Base64 del archivo, que además se manda en el header `Content-SHA256` |
Dos temas importantes:
1. En `GET`, la URI se firma tal como se envía, así que **el orden y el encoding de los parámetros
importan**. Construye la URL una sola vez y firma esa misma cadena.
2. En `POST`, la comparación se hace contra tu JSON re-serializado en forma compacta. Serializa una vez,
firma esa cadena, y **manda exactamente esos bytes**.
### Ejemplo completo [#ejemplo-completo]
```js
import { createHmac } from 'node:crypto';
const WEB_ID = process.env.FIRMAME_WEB_ID;
const API_KEY = process.env.FIRMAME_API_KEY;
function authorization(signedString) {
const hmac = createHmac('sha256', API_KEY).update(signedString, 'utf8').digest('base64');
return `signmage ${WEB_ID}:${hmac}`;
}
// POST: se firma el cuerpo compacto, y se manda ese mismo cuerpo.
const body = JSON.stringify({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
stickers: [
{
authority: 'EMAIL_LINK',
data: 'firmante@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
],
});
await fetch('https://app.firmame.com/developers/json', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authorization(body) },
body,
});
// GET: se firma la ruta con su query string, tal cual.
const path = '/api/document?from=1640995200000&to=1641945600000';
await fetch(`https://app.firmame.com${path}`, {
headers: { Authorization: authorization(path) },
});
```
Si recibes un `401`, la guía de [errores 401](/docs/autenticacion/errores-401) recorre las causas
posibles en orden.
# Código QR (https://devs.firmame.com/docs/guias/codigo-qr)
> Estampa un QR de verificación en el documento, en el mismo payload de creación.
Un QR estampado en el documento lleva a la página donde cualquiera puede verificar sus firmas. Viaja
en el mismo payload de `POST /developers/json` y no necesita un endpoint propio. Sirve cuando el
documento se va a imprimir: quien lo tenga en papel puede comprobar que es auténtico sin que le mandes
nada.
```js
await services.request({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
stickers: [ /* … */ ],
qr: [
{ width: 150, height: 150, x: 0, y: 0, page: 1 },
],
});
```
Las coordenadas siguen la misma convención que los stickers: **`x` e `y` se miden desde la esquina
inferior izquierda** de la página.
## Repetirlo en varias páginas [#repetirlo-en-varias-páginas]
Cada entrada de `qr` puede fijar una página o varias:
```js
qr: [
{ width: 150, height: 150, x: 0, y: 0, page: 1 }, // una página
{ width: 120, height: 120, x: 200, y: 400, pages: [2, 3, 4] }, // una lista
{ width: 120, height: 120, x: 400, y: 600, pages: 'all' }, // todas
]
```
Para un contrato largo que se archiva impreso, `pages: 'all'` deja cada hoja verificable por separado.
También puedes pedir el QR desde un [recuadro dibujado en el PDF](/docs/guias/recuadros-de-firma),
escribiendo `Firmamex QR` dentro del rectángulo. Es la vía cómoda cuando quien arma el documento decide
dónde va.
# Correos certificados (https://devs.firmame.com/docs/guias/correos-certificados)
> Envía documentación con constancia de entrega, apertura y lectura.
Un correo certificado deja evidencia de que mandaste algo, a quién, cuándo llegó y cuándo lo abrieron.
Sirve para notificaciones donde después vas a tener que demostrar que se entregaron: avisos de cobranza,
comunicaciones laborales, notificaciones contractuales.
## Cómo funciona [#cómo-funciona]
Firmame manda la documentación por ti, se queda con una copia de lo enviado, y va registrando lo que
pasa con ese correo: si el servidor del destinatario lo aceptó o lo rechazó, si lo abrió y si dio clic
en el acuse.
El proceso se cierra cuando el destinatario firma el acuse, o a las **24 horas** del envío si no lo
firmó — así puedes demostrar que realizaste la entrega. Al cerrar, Firmame te
manda por correo a ti y al destinatario un paquete con: una copia del correo enviado (en EML), el historial
completo en PDF y JSON, el acuse (firmado o no), y una **Constancia de Conservación NOM-151** que sella
con fecha y hora que esa información existía tal cual.
## Pedirlo [#pedirlo]
Se pide en el mismo `POST /developers/json`, agregando el objeto `certifiedEmail`.
```js
await services.request({
template: {
title: 'notificacion',
name: 'Envío de documentación',
},
fields: {
email: 'destinatario@dominio.com',
name: 'Ana Ruiz',
},
certifiedEmail: {
emailParams: [{ name: 'nombre', value: 'Ana Ruiz' }],
},
attachments: [
{
data: fs.readFileSync('anexo.pdf').toString('base64'),
name: 'anexo.pdf',
mimeType: 'application/pdf',
password: 'una-contrasena',
},
],
stickers: [ /* opcional: si además quieres que firmen */ ],
});
```
El documento puede venir de donde sea —`url_doc`, `b64_doc` o un template—, igual que en
[crear un documento](/docs/guias/crear-documento).
## Adjuntos [#adjuntos]
Cada entrada de `attachments` lleva el archivo en Base64, su nombre y su `mimeType`. El campo `password`
es opcional y protege ese adjunto; si lo usas, puedes recuperar la contraseña después:
```js
const { password } = await services.getAttachmentsPassword(firmamexId);
```
## La evidencia [#la-evidencia]
Cada paso de arriba queda registrado como un evento. Estos son los tipos que vas a ver en
`documentEventType`:
| Evento | Qué significa |
| ---------------------------------- | --------------------------------------------- |
| `DOCUMENT_CREATED` | Documento generado |
| `FLOW_STARTED` | Arrancó el flujo |
| `SIGNATURE_INVITATION` | Se envió la invitación a firma |
| `NON_SIGNER_RECIPENT_FLOW_STARTED` | Se notificó a un destinatario que no firma |
| `EMAIL_DELIVERED` | El servidor del destinatario aceptó el correo |
| `EMAIL_OPENED` | El destinatario abrió el correo |
| `EMAIL_CLICKED` | El destinatario dio clic en el correo |
| `EMAIL_FAILED` | El correo no se pudo entregar |
| `EMAIL_COMPLAINED` | El destinatario lo marcó como spam |
| `DOCUMENT_SIGNED` | Se firmó el documento |
| `DOCUMENT_COMPLETED` | Se completó el documento |
| `DOCUMENT_STAMPED` | Se selló el documento |
| `DOCUMENT_FLATTENED` | Se aplanó el documento (solo AcroForms) |
| `REJECTED_DOCUMENT` | El destinatario rechazó el documento |
| `FLOW_EXPIRED` | El flujo expiró |
`EMAIL_DELIVERED` significa que el servidor del destinatario aceptó el mensaje, no que la persona lo
haya visto. La lectura es `EMAIL_OPENED`.
## Recuperar los datos [#recuperar-los-datos]
```js
const data = await services.getCertifiedEmailData(firmamexId);
```
Trae los EML de los correos enviados y el JSON con los eventos generados. Lee el `Content-Type` de la
respuesta en lugar de dar por hecho que es JSON: refleja el formato en el que está guardada la evidencia.
## Verificar la Constancia de NOM-151 [#verificar-la-constancia-de-nom-151]
La Constancia de Conservación se puede verificar de forma independiente, sin depender de Firmame, en el
portal del PSC: [serviciospsc.cecoban.mx](https://serviciospsc.cecoban.mx:8004).
Ese portal pide dos archivos: el **Original** —[el historial en PDF](/docs/api/consultas/get-document-history-pdf),
el historial en JSON, el acuse y los correos enviados, unidos en orden alfabético en un solo archivo— y
la **Constancia de Conservación**, que es el archivo `.tsr` que llegó adjunto en el correo.
Armar el Original a mano es posible, pero es más simple hacerlo en
[verificar-correo-certificado.firmamex.com](https://verificar-correo-certificado.firmamex.com): subes
ahí los mismos archivos que recibiste por correo y genera un `Sellado.bin` listo para el portal del PSC.
Ese proceso solo une los archivos en orden alfabético — no modifica su contenido.
## Historial en cualquier momento [#historial-en-cualquier-momento]
Si solo quieres el avance sin la evidencia completa:
```js
const historial = await services.getHistory(firmamexId);
const pdf = await services.getHistoryAsPdf(firmamexId);
```
Ver [`GET /api/v1/document/{frmxId}/history/json`](/docs/api/consultas/get-document-history) para los
eventos y [`GET /api/v1/document/{frmxId}/history/pdf`](/docs/api/consultas/get-document-history-pdf)
para el mismo historial como PDF.
# Crear y enviar un documento (https://devs.firmame.com/docs/guias/crear-documento)
> De un PDF a un documento firmado, con Firmame llevando las invitaciones.
Esta guía cubre la integración más común: subes un contrato, defines quién firma y dónde, y Firmame se
encarga de invitar a la gente y avisarte cuando terminen.
## Lo mínimo que funciona [#lo-mínimo-que-funciona]
```js
const response = await services.request({
url_doc: {
url: 'https://ejemplo.com/contrato.pdf',
name: 'contrato.pdf',
},
stickers: [
{
authority: 'EMAIL_LINK',
data: 'firmante@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
],
});
```
La respuesta trae lo que necesitas guardar:
```json
{
"document_ticket": "c6279a7c-b3d5-4c71-b417-4687a4f38914",
"sticker_count": 1,
"stickers": [{ "sticker_id": "qv3ZzAL3w", "sticker_index": 0, "…": "…" }],
"document_url": "https://app.firmame.com/pdf/c6279a7c-b3d5-4c71-b417-4687a4f38914/"
}
```
Guarda **`document_ticket`**: es el identificador del documento en todo lo demás, y también llega como
`firmamex_id` en los webhooks. Guarda también los `sticker_id` si vas a querer
[corregir un firmante](/docs/guias/firmantes) más adelante.
## De dónde puede venir el PDF [#de-dónde-puede-venir-el-pdf]
Manda exactamente una de estas tres:
Firmame lo descarga. Útil cuando el PDF ya vive en tu almacenamiento.
```js
url_doc: {
url: 'https://ejemplo.com/contrato.pdf',
name: 'contrato.pdf',
}
```
El archivo viaja en la petición. Útil cuando lo acabas de generar y no está publicado.
```js
b64_doc: {
data: fs.readFileSync('contrato.pdf').toString('base64'),
name: 'contrato.pdf',
}
```
El documento se genera al vuelo a partir de una plantilla registrada. Ver
[Templates](/docs/guias/templates).
```js
template: {
title: 'contrato-arrendamiento',
name: 'Contrato — marzo',
},
fields: [
{ id: 'arrendatario', value: 'Ana Ruiz' },
]
```
## Dónde va cada firma [#dónde-va-cada-firma]
Hay dos formas de decirle a Firmame dónde va cada firma: dibujando el recuadro dentro del documento, o
mandando coordenadas explícitas en `stickers`.
### Recuadros automáticos [#recuadros-automáticos]
Si quien redacta o genera el documento puede marcar dónde va la firma, esta es la forma más simple:
[dibuja el recuadro dentro del PDF](/docs/guias/recuadros-de-firma) con la palabra `Firmamex` seguida
de los datos del firmante. Firmame lo encuentra, lo borra y coloca ahí el área de firma — tu código deja
de necesitar coordenadas. Así se ve en la práctica:
[PDF de ejemplo](https://storage.googleapis.com/static.firmamex.com/documentBoxes2.pdf).
### Con coordenadas [#con-coordenadas]
Si prefieres fijar la posición desde tu código, cada entrada de `stickers` es una persona firmando en
un lugar. Dos convenciones que conviene tener presentes al calcularla:
**`page` empieza en cero.** La primera página es `0`.
**`rect` va en coordenadas de PDF, con el origen abajo a la izquierda.** La mayoría de los editores
gráficos lo pone arriba, así que si tomas las coordenadas de una herramienta de diseño, invierte el eje
vertical.
```
ty ┌───────────────┐
│ │
│ la firma │
│ │
ly └───────────────┘
lx tx
(0,0) = esquina inferior izquierda de la página
```
Si no conoces de antemano el tamaño de la página —porque el PDF lo genera alguien más—
[`POST /api/pdf/pageData`](/docs/api/nom-151-y-herramientas-pdf/get-page-data) te da sus dimensiones para
calcular el rectángulo.
## Que Firmame mande las invitaciones [#que-firmame-mande-las-invitaciones]
Con lo de arriba tienes un documento firmable, pero el reparto de `document_url` queda de tu lado. Para
que Firmame invite, agrega un `workflow` y dale `email` a cada firmante:
```js
const response = await services.request({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
stickers: [
{
authority: 'EMAIL_LINK',
data: 'ana@dominio.com',
email: 'ana@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
{
authority: 'EMAIL_LINK',
data: 'luis@dominio.com',
email: 'luis@dominio.com',
page: 0,
rect: { lx: 330, ly: 150, tx: 530, ty: 250 },
},
],
workflow: {
subject: 'Contrato de arrendamiento',
message: 'Te compartimos el contrato para tu firma.',
remindEvery: '1d',
ordered: true,
signersConfig: [{ data: 'ana@dominio.com' }, { data: 'luis@dominio.com' }],
},
});
```
Con `ordered: true`, Luis no recibe su invitación hasta que Ana firma. Los detalles del flujo —
expiración, recordatorios, copias a terceros — están en [Flujos de firma](/docs/guias/flujos).
## Enterarte de que ya firmaron [#enterarte-de-que-ya-firmaron]
Registra un webhook para `document_completed` y Firmame te avisa cuando el último firmante termina:
```json
{
"notification_type": "document_completed",
"notification_id": "2698",
"firmamex_id": "c6279a7c-b3d5-4c71-b417-4687a4f38914",
"document_title": "contrato.pdf"
}
```
Con ese `firmamex_id` descargas el documento firmado, eligiendo `type`: `original` para el PDF con
las firmas tal como se aplicaron, o `universal` para esas mismas firmas envueltas en un sobre de
validación a largo plazo.
```js
const pdf = await services.downloadDocument(firmamexId, 'original');
```
Ver [Descargar un documento firmado](/docs/api/descargas/download-document) para los detalles del
endpoint, y [Webhooks](/docs/webhooks) para el ciclo completo y la política de reintentos.
## Descargar el historial [#descargar-el-historial]
El documento firmado es la mitad del resultado; la otra es su historial: cuándo se creó, cuándo salió
cada invitación, cuándo la abrió cada firmante y cuándo firmó. Con el mismo `firmamex_id` lo bajas en
dos formatos.
```js
const eventos = await services.getHistory(firmamexId);
const historial = await services.getHistoryAsPdf(firmamexId);
```
El **JSON** te da un arreglo de eventos, del más reciente al más viejo, para mover tu propia interfaz
sin esperar los webhooks: [`GET /api/v1/document/{frmxId}/history/json`](/docs/api/consultas/get-document-history).
Ahí está también el catálogo completo de tipos de evento.
El **PDF** es el mismo historial armado para entregárselo a alguien, y es el que un paquete de
verificación NOM-151 espera: [`GET /api/v1/document/{frmxId}/history/pdf`](/docs/api/consultas/get-document-history-pdf).
## Y después [#y-después]
# Endosos (https://devs.firmame.com/docs/guias/endosos)
> Transfiere un título de crédito ya firmado a un nuevo tenedor.
Un endoso es la transferencia de un título de crédito —típicamente un pagaré— de quien lo tiene a otra
persona. En papel se escribe al reverso y se firma; aquí es lo mismo, sobre un documento que ya está
completamente firmado.
## Requisitos [#requisitos]
El documento **tiene que estar completamente firmado** antes de endosarse. Si le falta una firma, la
petición se rechaza.
## Cómo se pide [#cómo-se-pide]
```js
await services.endorse(
firmamexId,
'Se hace constar que, en esta fecha, XX, S.A.P.I. de C.V., como tenedor, cedió el presente pagaré ' +
'por la totalidad del monto amparado por el mismo al que esta anotación va adherida.',
// endosante: quien transfiere
{
authority: 'SAT',
stickerType: 'line',
dataType: 'rfc',
email: 'endosante@dominio.com',
data: 'ARCX9012226P8',
imageType: 'endorsment',
page: 1,
rect: { lx: 145, ly: 496, tx: 443, ty: 610 },
},
// endosatario: quien recibe (opcional)
{
authority: 'SAT',
stickerType: 'line',
dataType: 'rfc',
email: 'endosatario@dominio.com',
data: 'BACX9012226P8',
imageType: 'stroke',
page: 1,
rect: { lx: 145, ly: 296, tx: 443, ty: 410 },
},
);
```
## Las piezas [#las-piezas]
**El texto del endoso** es la anotación que queda escrita en el documento. El campo se llama
`endorsment`.
**`endorser`** es el endosante, quien transfiere. Es un [sticker](/docs/guias/firmantes) normal con una
particularidad: dale `imageType: "endorsment"` para que el propio texto del endoso se dibuje como imagen
de firma, que es como se ve en un endoso de papel.
**`endorsee`** es el endosatario, quien recibe. Es **opcional**: un endoso en blanco no nombra
destinatario, y el título queda al portador.
Para operaciones con `SAT`, el `data` es el RFC de cada parte.
## Referencia [#referencia]
Todos los campos, en
[`POST /api/document/{firmamexId}/endorse`](/docs/api/firmantes-y-flujos/prepare-endorsement).
# Expedientes (https://devs.firmame.com/docs/guias/expedientes)
> Varios documentos, una sola invitación y un solo orden de firma.
Cuando un trámite abarca más de un documento —contrato, anexo y pagaré— mandarlos por separado significa
tres invitaciones, tres recordatorios y tres oportunidades de que alguien firme uno y olvide los otros.
Un expediente los junta en un solo paquete.
## El ciclo [#el-ciclo]
```js
// 1. Crear
const { document_set } = await services.createDocumentSet('Expediente de contratación');
// 2. Agregar cada documento — es el mismo POST /developers/json de siempre,
// más el campo document_set
await services.request({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
document_set,
stickers: [
{
authority: 'EMAIL_LINK',
data: 'ana@dominio.com',
email: 'ana@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
],
});
await services.request({
url_doc: { url: 'https://ejemplo.com/anexo.pdf', name: 'anexo.pdf' },
document_set,
stickers: [ /* … */ ],
});
// 3. Cerrar y arrancar el flujo
await services.closeDocumentSet({
documentSet: document_set,
workflow: {
remindEvery: '1d',
ordered: ['ana@dominio.com', 'luis@dominio.com'],
},
});
```
Los documentos entran por el mismo `POST /developers/json` que usas siempre; lo único que cambia es que
le pasas `document_set`. Si ya tienes código que crea documentos, agregar el campo es todo lo que hace
falta. Ver [Subir un documento a un expediente](/docs/guias/subir-documento-a-expediente) para el
detalle.
## El flujo abarca todo el paquete [#el-flujo-abarca-todo-el-paquete]
Al cerrar, el `workflow` es el [mismo objeto](/docs/guias/flujos) de un documento suelto, con una
diferencia: `ordered` lista firmantes del expediente completo, no de un documento en particular. Ana firma
todo lo que le toca, y hasta entonces le llega su turno a Luis.
El `workflow` es obligatorio al cerrar.
## Consultar el avance [#consultar-el-avance]
```js
const set = await services.getDocumentSet(document_set);
```
```json
{
"uuid": "9v3d6a47-e4e8-48ef-b9b3-7af45c8207dn",
"status": "CLOSED",
"documents": ["4a3d6a47-…", "5d08eabe-…"],
"signers": ["ana@dominio.com", "luis@dominio.com"],
"workflow": {
"currentSigner": "luis@dominio.com",
"lastNotification": 1665001495000,
"expirationDate": 1666211094000
}
}
```
`status` es `OPEN` mientras se le pueden agregar documentos y `CLOSED` una vez que arrancó el flujo.
El objeto `workflow` **solo aparece mientras el expediente está en proceso**: no viene si está abierto ni
cuando ya terminó, así que pregunta por él en vez de darlo por hecho.
## Precargar firmantes para todo el expediente [#precargar-firmantes-para-todo-el-expediente]
Precarga a las personas que deben firmar todos los documentos del expediente, sin ligarlas todavía a
una página o posición. Con el expediente en curso, arrastras el recuadro de cada firmante precargado
sobre cada documento desde la interfaz de Firmame; una vez colocados todos, cierras el expediente y
arranca el flujo.
```js
await services.addPremadeSticker(document_set, {
authority: 'EMAIL_LINK',
data: 'ana@dominio.com',
email: 'ana@dominio.com',
imageType: 'stroke',
stickerType: 'rect',
line: false,
width: 200,
height: 100,
});
```
Todavía no lleva `page` ni `rect` — solo `width` y `height` — porque la posición se define después, al
arrastrarlo en la interfaz. Todos los campos de arriba son obligatorios.
Devuelve un `uuid` propio; guárdalo si vas a querer quitar a esa persona:
```js
await services.deletePremadeSticker(document_set, { uuid: signerUuid });
```
Ver [Quitar un firmante del expediente](/docs/api/expedientes/delete-premade-sticker).
## Cambiar o cancelar el flujo del expediente [#cambiar-o-cancelar-el-flujo-del-expediente]
Igual que con un documento suelto, pero apuntando al UUID del expediente:
```js
await services.workflowSet(document_set, { expiration_date: '25/12/2026' });
await services.workflowSet(document_set, { cancel: true });
```
## Referencia [#referencia]
# Firmantes y autoridades (https://devs.firmame.com/docs/guias/firmantes)
> Cómo elige cada persona su método de verificación, y cómo corregir un firmante a tiempo.
La **autoridad** de un sticker define **cómo se comprueba la identidad del firmante**, y eso es lo que
queda registrado como evidencia de la firma. Un certificado del SAT y una liga enviada por correo no
comprueban lo mismo: el primero acredita a la persona ante una autoridad certificadora, el segundo
acredita el control de una cuenta de correo.
Es la decisión de más peso de cada sticker, porque determina dos cosas a la vez:
* **Qué respalda la firma.** El nivel de comprobación que puedes demostrar después, si alguien cuestiona
quién firmó.
* **Qué tan fácil es firmar.** Un certificado exige que la persona lo tenga a la mano; una liga se abre
desde el teléfono.
Elígela según lo que el documento necesite sostener, no solo por comodidad — y si el trámite tiene
requisitos legales concretos, confírmalos antes de decidir.
## Cuál elegir [#cuál-elegir]
| Autoridad | Qué queda comprobado |
| --------------- | ------------------------------------------------------------------------------ |
| `SAT` | La identidad de la persona ante el SAT, con su certificado y su llave privada |
| `OAUTH_TOKEN` | El control de una cuenta de Google o de Outlook, verificada por el proveedor |
| `WHATSAPP_PIN` | El control de un número, con un PIN de un solo uso al momento de firmar |
| `EMAIL_LINK` | El control de una cuenta de correo, por una liga única enviada a esa dirección |
| `WHATSAPP_LINK` | El control de un número, por una liga única enviada por WhatsApp |
| `SMS_LINK` | El control de un número, por una liga única enviada por SMS |
| `EMAIL` | El control de una cuenta de correo, por respuesta a un mensaje |
| `SMS` | El número del firmante, con la invitación enviada por SMS |
Están ordenadas de mayor a menor nivel de comprobación. Una liga acredita que quien firmó tenía acceso a
esa cuenta o a ese número; un certificado del SAT acredita a la persona ante la autoridad que lo emitió.
Con las autoridades **por liga** (`EMAIL_LINK`, `WHATSAPP_LINK`, `SMS_LINK`), Firmame envía la invitación
automáticamente y solo el firmante tiene la URL que autoriza firmar a su nombre.
Con `SAT`, `EMAIL` y `SMS`, si no configuras un [flujo](/docs/guias/flujos), repartir la URL de firma es
responsabilidad tuya.
También se aceptan los nombres largos en español —`Vinculada a Correo Electronico por Liga` y
similares—, que es lo que verás en integraciones viejas. En código nuevo usa el código corto.
## El campo `data` [#el-campo-data]
Es el valor que identifica a la persona, y su forma depende de la autoridad:
```js
{ authority: 'EMAIL_LINK', data: 'firmante@dominio.com' } // un correo
{ authority: 'WHATSAPP_PIN', data: '5216621234567' } // un teléfono
{ authority: 'SAT', data: 'VECJ880326XXX' } // un RFC
```
`email` es un campo aparte de `data`. **Es obligatorio si quieres que Firmame lleve el flujo**, porque es
a donde llega la invitación; con las autoridades por SMS y WhatsApp, omítelo — identifican por teléfono y
mandar los dos se rechaza.
## Cómo se ve la firma en el papel [#cómo-se-ve-la-firma-en-el-papel]
`imageType` controla el dibujo que queda estampado:
| Valor | Qué dibuja |
| -------- | -------------------------------------------------------------- |
| `desc` | Texto: «Firma realizada con certificado emitido por … para: …» |
| `name` | El nombre del certificado, en cursiva |
| `hash` | Un hash del documento seguido del nombre |
| `stroke` | El trazo que la persona dibuja al firmar |
Se pueden combinar dos con `+`, por ejemplo `stroke+desc` para el trazo con el texto descriptivo debajo.
## Verificación de identidad [#verificación-de-identidad]
Como capa extra de seguridad para confirmar la identidad de quien firma, agrega
`requireIdentityVerification: true` al sticker. El proceso completo —identificación oficial, prueba de
vida, créditos que consume y el webhook con el resultado— está en
[Verificación de identidad](/docs/guias/verificacion-de-identidad).
## Corregir un firmante [#corregir-un-firmante]
Mientras esa persona **no haya firmado**, puedes cambiarle el correo, el dato de verificación, la
autoridad o el tipo de imagen. Necesitas dos ids, los dos de la respuesta al crear el documento:
```js
await services.updateSigner(documentTicket, stickerId, {
authority: 'EMAIL_LINK',
email: 'correo-correcto@dominio.com',
data: 'correo-correcto@dominio.com',
imageType: 'desc',
});
```
Resuelve el caso de un correo mal escrito sin rehacer el documento. Una vez que la persona firmó, su
recuadro queda fijo; si aun así necesitas cambiarlo, cancela el flujo y genera el documento de nuevo.
Ver [`POST /api/{firmamexId}/signer/{publicSignerId}/update`](/docs/api/firmantes-y-flujos/update-signer).
## Otras opciones del sticker [#otras-opciones-del-sticker]
| Campo | Para qué |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `requireLocation` | Pide la ubicación del firmante antes de firmar |
| `sendEmailInvitation` | Manda el correo de invitación de este firmante |
| `customData` | Cadena tuya que se te regresa en los webhooks de esa persona, útil para amarrar la firma con un registro de tu base de datos |
# Flujos de firma (https://devs.firmame.com/docs/guias/flujos)
> Orden de firma, recordatorios, expiración y cancelación.
Un flujo le dice a Firmame **cómo** recolectar las firmas: en qué orden, cada cuánto insistir, cuándo
darse por vencido y a quién copiar. Sin flujo tienes un documento firmable y tú repartes las ligas. Con
flujo, Firmame se encarga.
Se puede adjuntar al crear el documento o agregarse después, con el mismo objeto en los dos casos.
```js
// al crear
await services.request({ url_doc: {...}, stickers: [...], workflow: {...} });
// o después, sobre un documento que ya existe
await services.workflow(documentTicket, {...});
```
## Orden de firma [#orden-de-firma]
`ordered: true` hace que cada persona reciba su invitación cuando la anterior firma. El orden lo marca
`signersConfig`:
```js
workflow: {
ordered: true,
signersConfig: [
{ data: 'ana@dominio.com' },
{ data: 'luis@dominio.com' },
{ data: 'VECJ880326XXX' },
],
}
```
Con `ordered: false` (el valor por defecto) todos reciben la invitación de inmediato y firman en
cualquier orden.
Cada entrada se empareja con un sticker por su valor `data`, así que tiene que coincidir exactamente con
lo que pusiste en el sticker.
## Watcher [#watcher]
Destinatarios no firmantes. Este campo es opcional, si se encuentra presente, estos correos recibiran una notificación cuando inicie y otra cuando termine el flujo, sin tener que ser parte de los firmantes
```js
workflow: {
watchers: ['legal@dominio.com', 'archivo@dominio.com'],
}
```
## Recordatorios y expiración [#recordatorios-y-expiración]
```js
workflow: {
subject: 'Contrato de arrendamiento',
message: 'Te compartimos el contrato para tu firma.',
remindEvery: '1d',
expirationDate: '25/11/2026',
language: 'es',
}
```
| Campo | Valores | Por defecto |
| ---------------- | ------------------------------------ | ------------------ |
| `remindEvery` | `none`, `12h`, o `Nd` con N de 1 a 7 | cada 24 h |
| `expirationDate` | `dd/MM/yyyy` o `dd/MM/yyyy HH:mm` | a 14 días |
| `language` | `es` o `en` | `es` |
| `subject` | hasta 150 caracteres | el asunto estándar |
| `message` | hasta 3500 caracteres | el texto estándar |
`expirationDate` es `dd/MM/yyyy`: **`12/11/2026` es 12 de noviembre**, no 11 de diciembre. Un formato
distinto se rechaza con `error parsing workflow expiration date`.
También puedes ajustar qué correos recibe cada quien:
```js
signersConfig: [
{ data: 'ana@dominio.com', notifications: 'ALL' },
{ data: 'luis@dominio.com', notifications: 'ONLY_START' },
]
```
Los valores son `ALL`, `ONLY_START`, `ONLY_END`, `START_AND_END`, `START_AND_REMINDERS_NO_END` y `NONE`.
## Qué se adjunta al final [#qué-se-adjunta-al-final]
```js
workflow: {
attachFiles: true, // el documento firmado (por defecto)
attachTimestamp: true, // el sello NOM-151
includeHistory: true, // el historial de eventos
alertAll: true, // avisar a todos al concluir (por defecto)
blockExpired: true, // impedir firmar después de la expiración
}
```
`includeHistory` adjunta el mismo PDF que devuelve
[`GET /api/v1/document/{frmxId}/history/pdf`](/docs/api/consultas/get-document-history-pdf).
## Editar un flujo en curso [#editar-un-flujo-en-curso]
Manda solo lo que quieras cambiar; el resto se queda igual. El caso más común es revivir un flujo que
expiró:
```js
await services.workflow(documentTicket, { expiration_date: '25/12/2026' });
```
Quitar el orden para que todos puedan firmar ya:
```js
await services.workflow(documentTicket, { ordered: false });
```
## Cancelar [#cancelar]
```js
await services.workflow(documentTicket, { cancel: true });
```
`cancel: true` corta todo lo demás: cuando lo mandas, no se lee ninguna otra propiedad del objeto. Manda
la cancelación sola, y si vas a reconfigurar, hazlo en una llamada aparte.
## Un flujo para varios documentos [#un-flujo-para-varios-documentos]
Si el trámite abarca más de un documento —contrato, anexo y pagaré— conviene un
[expediente](/docs/guias/expedientes): una sola invitación, un solo orden y una sola conclusión para todo
el paquete.
La lista completa está en el [esquema `Workflow`](/docs/api/firmantes-y-flujos/update-workflow).
# Gestor (https://devs.firmame.com/docs/guias/gestor)
> Asigna quién administra el documento dentro de tu organización.
Por defecto, el documento pertenece a la cuenta cuyo Web ID firmó la petición. Con `owner` lo asignas a
una persona concreta de tu organización, que será quien lo administre desde la aplicación de Firmame.
Es útil cuando los documentos se generan de forma externa — por ejemplo, desde tu backend con las
credenciales de una sola cuenta — pero cada usuario necesita verlos en su propio repositorio dentro de
Firmame: sin `owner`, todos quedarían bajo la cuenta que firmó la petición en lugar de la persona a la
que realmente le corresponden.
```js
await services.request({
url_doc: { url: 'https://ejemplo.com/contrato.pdf', name: 'contrato.pdf' },
owner: { email: 'gestor@tuempresa.com' },
});
```
Para que funcione hacen falta dos cosas:
1. Que esa persona ya esté registrada como parte de tu organización.
2. Que uses el mismo correo con el que se registró.
## Errores [#errores]
| Mensaje | Qué pasó | Qué hacer |
| ---------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
| `invalid owner` | No se encontró a esa persona en la organización | Revisa el correo; tiene que ser el del registro |
| `can't verify user at this moment` | La verificación no se pudo completar | Reintenta más tarde |
# NOM-151 y herramientas PDF (https://devs.firmame.com/docs/guias/nom151)
> Sella cualquier archivo o hash con constancia de conservación, y utilidades de PDF.
Un sello NOM-151 certifica ante un tercero que algo existía, con ese contenido exacto, en un momento
dado. Firmame lo aplica solo a los documentos que firma; estos endpoints te dejan usarlo por separado,
sobre archivos que nunca pasaron por la plataforma.
## Sellar un hash [#sellar-un-hash]
El caso más flexible: sacas tú el SHA-256 de lo que sea —un registro de base de datos, una bitácora, un
artefacto de compilación— y mandas solo el digest.
```js
import { createHash } from 'node:crypto';
const hash = createHash('sha256').update(datos).digest('hex');
const sello = await services.timestamp({ hash });
```
**Firmame nunca ve tus datos, solo el digest.** Eso lo hace viable para información que no puedes
compartir.
Ojo con el formato: aquí el hash va en **hexadecimal**.
### Validarlo después [#validarlo-después]
Puedes validar contra el archivo, o contra su hash si prefieres no subirlo:
```js
// con el archivo
await services.timestampValidate({ file: contenido, timestamp: sello.timestamp });
// solo con el hash
await services.timestampValidate({ hash, timestamp: sello.timestamp });
```
## Sellar un PDF [#sellar-un-pdf]
Si lo que tienes es un PDF y quieres el sello embebido en el archivo:
```js
const sellado = await services.nom151Stamp(fs.readFileSync('documento.pdf'));
```
Y para verificar uno que ya viene sellado:
```js
const resultado = await services.nom151Validate(fs.readFileSync('sellado.pdf'));
```
`POST /api/nom151/stamp`, `/api/nom151/validate`, `/api/print` y `/api/pdf/pageData` reciben el archivo
como `multipart/form-data`. En multipart la cadena firmada es el SHA-256 en Base64 del archivo, que
además va en el header `Content-SHA256` — los SDKs se encargan de esto por ti. El playground funciona con
los endpoints JSON.
## Los sellos de un documento de Firmame [#los-sellos-de-un-documento-de-firmame]
Para los documentos que sí pasaron por la plataforma:
```js
const { timestamps } = await services.getNom151StampsForDocument(firmamexId);
```
```json
{
"timestamps": [
{
"hash": "ce92da77b45ddbb26ab4210600023de8f0effb062276cd4c2765feed3988cfc4",
"tsa": "L=MIGUEL HIDALGO,ST=CIUDAD DE MEXICO,C=MX,CN=CECOBAN-PSC-NOM151-1-01,O=CECOBAN SA DE CV",
"genTime": "2023-07-10T18:21:28.467Z",
"status": "granted",
"policy": "2.16.484.101.10.316.2.3.1.2.1.1.1"
}
]
}
```
Con el `hash` de un sello bajas la revisión exacta del documento que ese sello ampara:
```js
const revision = await services.getDocumentForStamp(firmamexId, timestamps[0].hash);
```
Eso es lo que necesita un tercero para verificar: el documento como estaba en ese momento, no como está
ahora.
## Herramientas PDF [#herramientas-pdf]
### Dimensiones de una página [#dimensiones-de-una-página]
Antes de colocar stickers en un documento que no generaste tú, conviene saber de qué tamaño es la página
— `rect` va en unidades de PDF desde la esquina inferior izquierda, y una página rotada o de tamaño
inusual deja un rectángulo fijo en el lugar equivocado.
```js
const primera = await services.getPageData(archivo, 'first');
const segunda = await services.getPageData(archivo, '2');
const ultima = await services.getPageData(archivo, 'last');
```
Acepta `first`, `last` o un número de página. **Aquí los números empiezan en 1**, a diferencia del `page`
de un sticker, que empieza en 0.
### Aplanar un PDF [#aplanar-un-pdf]
Convierte los campos de formulario en contenido estático y fusiona las capas:
```js
const plano = await services.printPdf(fs.readFileSync('formulario.pdf'));
```
Útil antes de sellar o firmar un documento con formulario interactivo que no quieres que quede editable.
## Referencia [#referencia]
# Recuadros de firma en el documento (https://devs.firmame.com/docs/guias/recuadros-de-firma)
> Coloca las áreas de firma dibujándolas dentro del PDF, sin mandar coordenadas por API.
Hay dos formas de decirle a Firmame dónde va cada firma. La documentada en la referencia de API es el
arreglo `stickers`, con coordenadas explícitas. La otra es dibujar los recuadros **dentro del documento**
y dejar que Firmame los encuentre.
Esta casi siempre es mejor, ya que no se tienen que recalcular coordenadas y es especialmente útil si el documento lo produce alguien más — un área legal, una
plantilla de Word, un generador de contratos.
También puedes generar los PDFs en la herramienta de tu preferencia, o usar nuestros Templates para generar estos recuadros
## Cómo funciona [#cómo-funciona]
Al recibir un PDF, Firmame analiza **las primeras 3 y las últimas 8 páginas** buscando recuadros. Un
recuadro Firmame es cualquier rectángulo que contenga la palabra `Firmamex` seguida de los datos del
firmante, o de `QR`.
Cuando encuentra uno, lo borra y coloca en su lugar el área de firma o el código QR del documento
Si tus recuadros están fuera de ese rango de páginas, o quieres que el análisis sea más rápido, manda
`sticker_pages` en `POST /developers/json`. Empieza en cero y admite hasta 30 páginas: `[0, 2, 4]` son las
páginas 1, 3 y 5.
## La sintaxis [#la-sintaxis]
```
Firmamex [datos del firmante] [configuración]
```
### Datos del firmante [#datos-del-firmante]
Va el dato en sí, no el nombre del tipo. Firmame deduce la autoridad de la forma del dato:
| Dato | Autoridad por defecto |
| --------------------------- | ------------------------------------------------------------------------ |
| RFC, p. ej. `VECJ880326XXX` | SAT. Agrega también un correo si quieres que Firmame mande la invitación |
| Correo | Correo por liga |
| Teléfono a 10 dígitos | SMS por liga |
### Configuración [#configuración]
Todas opcionales, y se puede combinar **una de cada categoría**.
**Cambiar la autoridad**
| Palabra | Efecto |
| ------------- | ------------------------------------------------------------ |
| `sso` | Correo por liga → Correo por Identity Provider |
| `certificado` | Correo por liga → Correo; SMS por liga → SMS |
| `pin` | Correo por liga → Correo por PIN; SMS por liga → SMS por PIN |
| `wa` | SMS por liga → WhatsApp por liga |
| `waotp` | SMS por liga → WhatsApp por PIN |
**Proceso de firma**
| Palabra | Efecto |
| ------- | ---------------------------------------------------------------------------------------- |
| `riv` | Exige [verificación de identidad](/docs/guias/verificacion-de-identidad) antes de firmar |
**Imagen de la firma**
| Palabra | Efecto |
| ------------- | ------------------------------------------------------------- |
| `desc` | Texto descriptivo |
| `dibujo` | El firmante traza su firma o sube un PNG |
| `solo_dibujo` | El firmante traza su firma, sin opción de subir imagen |
| `dibujo+desc` | El trazo, con el texto descriptivo debajo |
| `hash` | Un hash del documento como imagen |
| `name` | El nombre del certificado, en cursiva |
| `desc_data` | Emisor del certificado, número de serie y nombre del firmante |
| `qr` | QR del documento, único para esa firma |
**Alineación**
| Palabra | Efecto |
| ---------- | ------------------------------------------------ |
| `linea` | El trazo queda en la parte inferior del recuadro |
| `centrada` | El trazo queda centrado en el recuadro |
**Repetir en varias páginas**
Páginas separadas por `-` o `,`, por ejemplo `1-3,9`. El recuadro aparece en esas páginas, en la misma
posición.
## Ejemplos [#ejemplos]
```
Firmamex firmante@correo.com
```
Invitación por correo por liga, sin especificar trazo.
```
Firmamex VECJ880326XXX firmante@correo.com dibujo
```
Invitación a `firmante@correo.com` para firmar con su certificado del SAT, dibujando su firma.
```
Firmamex 521234567890 pin name
```
Invitación por SMS por PIN, con el nombre del certificado como trazo.
## En un template HTML [#en-un-template-html]
El mismo mecanismo sirve en los [templates](/docs/api/templates/save-template). Por ejemplo, con templates HTML, defines el recuadro en la posición donde quieres la firma:
```html
```
El texto va en blanco y a 2px a propósito: no es necesario que se vea el recuadro, solo
tiene que existir para que Firmame lo encuentre.
Al generar el documento inyectas ahí el contenido del recuadro como cualquier otro campo:
```js
const response = await services.request({
template_title: 'ejemploSticker',
sticker_pages: [0, 2, 4],
fields: [
{ id: 'signer1', value: 'Firmamex GOCF9002226A7 dibujo' },
],
tables: [],
});
```
## Archivos de ejemplo [#archivos-de-ejemplo]
* [Ejemplo en PDF](https://storage.googleapis.com/static.firmamex.com/documentBoxes2.pdf)
* [Ejemplo en documento de texto](https://storage.googleapis.com/static.firmamex.com/documentBoxes.docx)
# Subir un documento a un expediente (https://devs.firmame.com/docs/guias/subir-documento-a-expediente)
> El mismo POST /developers/json de crear un documento, con document_set obligatorio.
No hay un endpoint distinto para "agregar" un documento a un expediente: es exactamente
`POST /developers/json`, el mismo que usas para un documento suelto, con un campo más.
## Lo mínimo que funciona [#lo-mínimo-que-funciona]
```js
const response = await services.request({
url_doc: {
url: 'https://ejemplo.com/contrato.pdf',
name: 'contrato.pdf',
},
document_set: '9v3d6a47-e4e8-48ef-b9b3-7af45c8207dn',
stickers: [
{
authority: 'EMAIL_LINK',
data: 'firmante@dominio.com',
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
},
],
});
```
Sin `document_set`, el documento se crea suelto — fuera del expediente. El valor es el `uuid` que
devolvió [`POST /api/documentset`](/docs/api/expedientes/create-document-set) al crear el expediente, y
tiene que seguir **abierto** (`status: "OPEN"`): una vez cerrado, ya no acepta documentos nuevos. Ver
[Consultar expediente](/docs/api/expedientes/get-document-set) para confirmar el estado.
La respuesta es la misma que al crear un documento suelto: guarda `document_ticket` y los `sticker_id` de
cada firmante.
## Todo lo demás es igual que crear un documento suelto [#todo-lo-demás-es-igual-que-crear-un-documento-suelto]
El origen del PDF (`url_doc`, `b64_doc` o `template`), cómo se ubica cada `sticker` con `page` y `rect`, y
las autoridades de firma no cambian por estar dentro de un expediente. Ver
[Crear y enviar un documento](/docs/guias/crear-documento) para el detalle de cada uno.
## No mandes `workflow` aquí [#no-mandes-workflow-aquí]
El flujo de firma del expediente se define **una sola vez, al cerrarlo** — no por documento. Si mandas
`workflow` en esta llamada, estás armando el flujo de un documento suelto, que no es lo que quieres
dentro de un expediente. Ver [Expedientes](/docs/guias/expedientes#el-flujo-abarca-todo-el-paquete) para
cómo cerrar el expediente y arrancar las invitaciones para todo el paquete de una vez.
## Y después [#y-después]
# Templates (https://devs.firmame.com/docs/guias/templates)
> Registra un documento una vez y genera cada instancia mandando solo los datos.
Si mandas el mismo documento cambiando unos cuantos datos, no hace falta que lo generes y lo subas cada
vez. Regístralo como template una sola vez, y a partir de ahí mandas nada más los valores: Firmame arma
el PDF.
## El ciclo [#el-ciclo]
## Los cuatro tipos [#los-cuatro-tipos]
Un solo endpoint —`POST /developers/template/save`— los atiende a todos. Lo que cambia es
`template_type` y cómo va codificado el archivo.
| Tipo | Qué es | Cómo se marcan los campos |
| --------- | ------------------------------------------------------ | ----------------------------------------------------- |
| `html` | Un HTML con CSS en línea | `class="signmage-template-field"` y un `id` único |
| `pdf` | Un PDF con formulario AcroForm | Nada que marcar: Firmame detecta los campos dinámicos |
| `pdfmake` | Una definición de [pdfmake](https://pdfmake.github.io) | `{{variable}}` |
| `docx` | Un documento de Word | Campos de correspondencia y controles de contenido |
### HTML [#html]
Marca cada hueco con la clase y un `id`:
```html
(aquí va el valor)
es el arrendatario de este contrato.
```
Las tablas dinámicas usan `class="signmage-template-table"` con su propio `id`.
```js
await services.saveTemplate({
template_type: 'html',
template_name: 'contrato-arrendamiento',
template_data: Buffer.from(html).toString('base64'),
});
```
### PDF con AcroForm [#pdf-con-acroform]
Si ya tienes un PDF con formulario, no hay que marcar nada — Firmame lee los campos que el formulario
declara, con su tipo y sus valores permitidos.
```js
await services.saveTemplate({
template_type: 'pdf',
template_name: 'solicitud',
url_doc: 'https://ejemplo.com/solicitud-con-formulario.pdf',
});
```
`url_doc` evita el sobrecosto de Base64, que en un PDF grande no es menor.
### pdfmake [#pdfmake]
```js
await services.saveTemplate({
template_type: 'pdfmake',
template_name: 'pagare',
template_data: {
content: [
'El suscrito {{nombre}} se obliga a pagar el día {{fecha}}.',
],
},
});
```
Ojo: en `pdfmake` el `template_data` va como objeto JSON crudo, **no** en Base64.
### DOCX [#docx]
Marca cada hueco con uno de los dos mecanismos nativos de Word — Firmame usa el nombre que le des al
campo como identificador del placeholder, igual que el `id` en HTML:
* **Campos de correspondencia**: pestaña **Correspondencia → Insertar campo combinado**, o
**Insertar → Elementos rápidos → Campo → MergeField** para escribir el nombre a mano sin necesitar
una lista de datos. En el documento se ven como `«nombre_del_campo»`.
* **Controles de contenido**:
1. Activa la pestaña **Programador**: **Archivo → Opciones → Personalizar cinta de opciones** y
marca la casilla «Programador».
2. En **Programador → Controles**, inserta un **control de contenido de texto sin formato** o de
**texto enriquecido** en el punto donde va el valor.
3. Con el control seleccionado, abre **Propiedades** y escribe el nombre del campo en **Etiqueta**
(`Tag`) — ese es el identificador que Firmame lee.
```js
await services.saveTemplate({
template_type: 'docx',
template_name: 'carta-oferta',
template_data: fs.readFileSync('carta.docx').toString('base64'),
});
```
## Qué te devuelve [#qué-te-devuelve]
Los placeholders que encontró, con el valor vacío.
Arreglo de `{id, value}`, más un arreglo `tables`:
```json
{
"template_title": "contrato-arrendamiento",
"fields": [{ "id": "arrendatario", "value": "" }],
"tables": [{ "id": "conceptos", "rows": [] }]
}
```
Arreglo de `{key, value, type}`, con `field_options` en los campos de opción:
```json
{
"template_title": "solicitud",
"fields": [
{ "key": "nombre_completo", "value": "", "type": "text" },
{ "key": "regimen", "value": "", "type": "combobox",
"field_options": ["Persona física", "Persona moral"] }
]
}
```
Objeto plano indexado por el nombre del placeholder:
```json
{
"template_title": "pagare",
"fields": { "nombre": "", "fecha": "" }
}
```
Esa misma forma es la que devuelves al generar el documento.
## Generar un documento [#generar-un-documento]
Es el `POST /developers/json` de siempre, con `template` en lugar de `url_doc`:
```js
await services.request({
template: {
title: 'contrato-arrendamiento',
name: 'Contrato — Ana Ruiz',
},
fields: [
{ id: 'arrendatario', value: 'Ana Ruiz' },
{ id: 'monto', value: '$12,500.00' },
],
stickers: [ /* … */ ],
});
```
Como es el mismo endpoint, puedes combinarlo con todo lo demás: stickers, flujo, expediente, QR.
## Consultar los templates de tu cuenta [#consultar-los-templates-de-tu-cuenta]
```js
const templates = await services.listTemplates();
const fields = await services.getTemplateFields('contrato-arrendamiento');
```
Útil para construir un formulario en tu propia interfaz sin tener los nombres de los campos escritos a
mano.
## Referencia [#referencia]
# Verificación de identidad (https://devs.firmame.com/docs/guias/verificacion-de-identidad)
> Comprueba que el firmante es una persona real, con identificación oficial y prueba de vida.
Puedes pedirle al firmante una verificación de identidad antes de firmar: es una capa extra de
seguridad que confirma quién es, además de la [autoridad](/docs/guias/firmantes) de firma que ya usas.
No la sustituye — se combina con cualquiera de ellas —, y la activas cuando el trámite necesita ese
nivel adicional de certeza.
## Cómo activarla [#cómo-activarla]
Agrega `requireIdentityVerification: true` al sticker:
```js
{
authority: 'EMAIL_LINK',
data: 'firmante@dominio.com',
email: 'firmante@dominio.com',
requireIdentityVerification: true,
page: 0,
rect: { lx: 330, ly: 300, tx: 530, ty: 400 },
}
```
Si en cambio defines el sticker [dibujando el recuadro dentro del PDF](/docs/guias/recuadros-de-firma), el
equivalente es la palabra clave `riv`:
```
Firmamex firmante@correo.com riv
```
Es un campo opcional y por defecto `false` — nada cambia si no lo mandas. Hoy solo está disponible en el
sticker que se manda al crear un documento (`POST /developers/json`); los stickers premade de un
expediente no lo incluyen.
## El proceso [#el-proceso]
El firmante pasa por dos pasos antes de poder firmar:
* **Identificación oficial.** Sube una identificación vigente, legible y a color — una fotocopia, una
imagen borrosa o un documento vencido no pasan.
* **Prueba de vida.** Una selfie tomada en el momento, para comprobar que quien la toma es una persona
real —no una foto, no una pantalla, no otra identificación— y que corresponde a la persona de la
identificación.
Firmame corre varias verificaciones independientes sobre esos dos elementos, y el firmante puede
reintentar si alguna no pasa. El catálogo completo de verificaciones y de los mensajes que puede devolver
cada una está en la referencia del webhook, más abajo.
## Créditos [#créditos]
Cada verificación de identidad consume créditos adicionales de tu cuenta, aparte de los que usa el
documento.
## El webhook [#el-webhook]
El resultado no llega en la respuesta de la API — llega por webhook:
* **`identity_verification_completed`** — el firmante ya quedó verificado y puede firmar.
* **`identity_verification_failed`** — alguna verificación no pasó.
Ambos comparten la misma forma: `meta` es un arreglo con **un intento por entrada**, así que un firmante
que falla y reintenta no genera varias notificaciones — genera una sola, con el arreglo más largo.
```json
{
"notification_type": "identity_verification_completed",
"notification_id": "2701",
"firmamex_id": "04337396-e929-43ca-9b8a-ec958a44079d",
"document_title": "documento.pdf",
"meta": [
{
"status": "FAILURE",
"date": "2025-03-24T04:11:42.000Z",
"signer": "firmante@dominio.com",
"errors": [{ "category": "USABILITY", "message": "PHOTOCOPY" }]
},
{
"status": "SUCCESS",
"date": "2025-03-25T10:38:11.000Z",
"signer": "firmante@dominio.com",
"errors": []
}
]
}
```
Como `meta` guarda todo el historial, en `identity_verification_completed` **lee la entrada con
`status: "SUCCESS"`** en vez de asumir que solo hay una — puede venir precedida de varios intentos
fallidos, como en el ejemplo. En `identity_verification_failed`, en cambio, todas las entradas son
fallos, y el evento trae además `first_notification_date`, la fecha del primer intento de esa serie.
Cada `error` trae una `category` (qué verificación falló: identificación oficial o prueba de vida) y un
`message` con el detalle — el catálogo completo de ambos está documentado en la referencia.
## Referencia [#referencia]
# Cómo funcionan (https://devs.firmame.com/docs/webhooks)
> Firmame te avisa cuando algo pasa; tú usas ese aviso para descargar el documento.
Un webhook es una URL tuya a la que Firmame hace `POST` cuando ocurre algo: alguien firmó, el documento
se completó, una invitación rebotó. Es la alternativa a estar consultando el estado cada tanto.
## Configurar tu URL [#configurar-tu-url]
Entra a tu cuenta de desarrollador en [firmamex.com/developers/login](https://firmamex.com/developers/login)
y registra ahí la URL. Es una sola por cuenta, y recibe **todas** las notificaciones de tus documentos.
La URL tiene que aceptar `POST` con `Content-Type: application/json`.
## El ciclo [#el-ciclo]
La notificación **no trae el documento**, trae el `firmamex_id` con el que lo pides. Esa segunda
llamada va autenticada como cualquier otra.
## Lo que llega [#lo-que-llega]
Todos los eventos comparten el mismo sobre:
```json
{
"notification_type": "document_completed",
"notification_id": "2698",
"firmamex_id": "04337396-e929-43ca-9b8a-ec958a44079d",
"document_title": "contrato.pdf",
"text": "Document 04337396-… has been completed"
}
```
| Campo | Qué es |
| ------------------- | --------------------------------------------------------------------- |
| `notification_type` | Qué pasó |
| `notification_id` | Id único de la notificación |
| `firmamex_id` | El id del documento — el mismo `document_ticket` de cuando lo creaste |
| `document_title` | El nombre del archivo |
| `meta` | Solo en algunos eventos; lleva el detalle específico |
Los eventos de firma traen además `signer_email`, `signer_fingerprint` y `signer_data`.
Como llegan todos los tipos a la misma URL, **discrimina por `notification_type`** al recibir.
## Obtener el documento [#obtener-el-documento]
Con el `firmamex_id` de la notificación descargas el documento firmado en
[`GET /api/document/{type}/{ticket}`](/docs/api/descargas/download-document), eligiendo `type`:
`original` para el PDF con las firmas tal como se aplicaron, o `universal` para esas mismas firmas
envueltas en un sobre de validación a largo plazo.
```js
const pdf = await services.downloadDocument(firmamexId, 'original');
```
*Original* y *universal* son dos representaciones firmadas del mismo documento; la diferencia está en
[Conceptos](/docs/conceptos#original-y-universal).
## Qué evento escuchar [#qué-evento-escuchar]
Para la mayoría de las integraciones, **`document_completed`** es el único que importa: se dispara cuando
ya firmaron todos y el documento está listo.
`original_signed` llega una vez por cada firma, así que en un documento de tres firmantes lo vas a
recibir tres veces. Úsalo si quieres mostrar avance parcial; no lo uses para disparar tu proceso de
negocio.
Los eventos de error —`email_error`, `sms_error`, `whatsapp_error`— avisan que una invitación no llegó.
Vale la pena atenderlos: sin ellos, un correo mal escrito se ve igual que un firmante que se está
tardando.
El catálogo completo, con el payload de cada uno, está en la
[referencia](/docs/api/webhooks-y-cuenta/document-completed).
## Qué espera Firmame de tu endpoint [#qué-espera-firmame-de-tu-endpoint]
* Responde **`200`**. Cualquier otra cosa cuenta como fallo.
* Responde rápido. Si vas a hacer trabajo pesado, encólalo y contesta el `200` de inmediato.
* Responde `200` **también** cuando llegue un evento que no esperabas. Los tipos nuevos no deberían
tumbar tu endpoint.
### Reintentos [#reintentos]
Si tu servidor no contesta `200`, Firmame reintenta con backoff exponencial:
```
10 segundos + min(2 ^ intentos, 600) + jitter
```
El tiempo entre intentos crece hasta un máximo de **600 segundos (10 minutos)**, con hasta **30
intentos**. En la práctica eso te da varias horas de margen para levantar un servicio caído sin perder
notificaciones.
**Una URL inválida no se reintenta.** Si el registro apunta a un host que no resuelve, no hay nada que
reintentar — vale la pena verificar la URL al registrarla.
### Idempotencia [#idempotencia]
`notification_id` es único por notificación. Guárdalo al procesar y descarta los repetidos: es la forma
más simple de que un reintento no cobre dos veces, no mande dos correos, ni cree dos registros.
## Probarlo [#probarlo]
Durante el desarrollo, un túnel hacia tu máquina (ngrok, Cloudflare Tunnel o similar) te deja registrar
una URL pública que apunta a tu servidor local y ver las notificaciones llegar de verdad.
## Suscripciones por evento [#suscripciones-por-evento]
Existe además un esquema de **suscripciones**, donde registras una URL distinta por tipo de evento vía
API en lugar de una sola en el panel. Es lo que usan integraciones como Zapier, y está en
[beta](/docs/webhooks/suscripciones).
# Suscripciones por evento (beta) (https://devs.firmame.com/docs/webhooks/suscripciones)
> Registra una URL distinta por tipo de evento vía API, en lugar de una sola en el panel.
Este esquema está en beta. Es el que usan integraciones como Zapier, y su comportamiento puede cambiar.
Para una integración en producción, el [webhook único](/docs/webhooks) es la vía estable.
En lugar de una URL en el panel que recibe todo, aquí registras **una URL por tipo de evento** desde la
API. Sirve cuando distintos eventos deben ir a distintos destinos — por ejemplo, las firmas a tu backend
y los errores de envío a una herramienta de monitoreo.
## Cómo cambia el comportamiento [#cómo-cambia-el-comportamiento]
En cuanto registras la primera suscripción, tu cuenta pasa al esquema de suscripciones y **la URL única
del panel deja de recibir**. A partir de ahí, un evento se entrega solo si tiene una suscripción
registrada; los tipos que no suscribas no se entregan.
Por eso conviene planearlo: antes de registrar la primera, ten listas las suscripciones de todos los
eventos que hoy procesas. Borrarlas después no regresa la cuenta al esquema anterior — si necesitas
volver, escríbenos a **[soporte@firmamex.com](mailto:soporte@firmamex.com)**.
## Registrar [#registrar]
```js
await services.addWebhook({
url: 'https://tu-servidor.com/hooks/firmas',
type: 'document_completed',
});
```
La respuesta trae el id de la suscripción. Guárdalo: es lo único con lo que puedes darla de baja.
Una URL puede repetirse en varios tipos, y un tipo puede tener varias URLs — se entrega a todas las
registradas para ese evento.
```js
// el mismo destino para dos eventos
await services.addWebhook({ url: BACKEND, type: 'document_completed' });
await services.addWebhook({ url: BACKEND, type: 'document_rejected' });
// y los errores de envío a otro lado
await services.addWebhook({ url: MONITOREO, type: 'email_error' });
```
## Dar de baja [#dar-de-baja]
```js
await services.deleteWebhook({ id: subscriptionId });
```
`DELETE /api/v1/webhooks` lleva el id **en el cuerpo JSON**, no en la ruta ni en el query string. Algunos
clientes HTTP omiten el cuerpo en `DELETE`, así que verifica que el tuyo sí lo mande.
Dar de baja un id que ya no existe también responde correctamente.
## Tipos de evento [#tipos-de-evento]
Los mismos del [catálogo](/docs/api/webhooks-y-cuenta/document-completed): `document_completed`,
`original_signed`, `universal_signed`, `document_rejected`, `workflow_expired`, los tres de error de
envío, los dos de NOM-151 y los dos de verificación de identidad.
El [esquema `WebhookType`](/docs/api/webhooks-y-cuenta/add-webhook) lista los valores aceptados.
## Lo demás no cambia [#lo-demás-no-cambia]
El sobre de la notificación, la descarga del documento, la política de reintentos y la idempotencia
funcionan igual que en el [esquema estándar](/docs/webhooks).
## Referencia [#referencia]
# Obtener un documento desde una notificación de webhook (https://devs.firmame.com/docs/api/consultas/get-data-by-webhook)
`POST https://app.firmame.com/developers/webhook`
Requiere el header `Authorization` del esquema `signmage`.
Convierte el `notification_id` de un webhook en el documento mismo. Este es el paso natural
después de recibir una notificación: el webhook te dice *que* pasó algo, esto te dice *qué*.
`action` elige qué representación obtienes, y si te la dan en bytes o en JSON:
| `action` | Devuelve |
|---|---|
| `download_original_bytes` | El PDF firmado **original**, en bytes |
| `download_original_document` | El original, como metadatos JSON |
| `download_universal_bytes` | El PDF **universal** (encapsulado), en bytes |
| `download_universal_document` | El universal, como metadatos JSON |
| `download_document_bytes` | El documento tal como está guardado, en bytes |
| `download_document` | El documento tal como está guardado, como metadatos JSON |
*Original* y *universal* son dos representaciones firmadas del mismo documento: el original
lleva las firmas tal como se aplicaron, el universal las envuelve en un sobre de validación a
largo plazo.
## Cuerpo de la petición
`application/json`:
- `notification_id` (string, requerido) — El `notification_id` que venía en el payload del webhook.
- `action` (string, requerido, `download_original_bytes` | `download_original_document` | `download_universal_bytes` | `download_universal_document` | `download_document` | `download_document_bytes`)
Ejemplo — Descargar el PDF firmado:
```json
{
"notification_id": 84512,
"action": "download_original_bytes"
}
```
Ejemplo — Obtener los metadatos en vez del archivo:
```json
{
"notification_id": 84512,
"action": "download_original_document"
}
```
## Respuestas
### `200`
El PDF en las acciones `*_bytes`, servido como `application/pdf` con un header
`Content-disposition` de adjunto; metadatos JSON en las demás.
`application/pdf` — `string`.
`application/json`:
- `name` (string, opcional) — The document's original file name.
- `signatures_count` (integer, opcional) — How many signatures the document carries.
- `data` (string, opcional, format `byte`) — The PDF, Base64-encoded.
- `error` (string, opcional) — Present instead of the other fields when no document matched.
Ejemplo:
```json
{
"name": "contrato.pdf",
"signatures_count": 2,
"data": "JVBERi0xLjcNCiW1tbW1…"
}
```
Ejemplo:
```json
{
"error": "no_document_found"
}
```
### `204`
La petición no traía cuerpo.
### `400`
O faltó `action` —cuerpo `action is missing`— o ninguna notificación correspondió a un
documento —cuerpo `no_document_found`—. Los dos en texto plano.
`text/plain` — `string`.
Ejemplo:
```
action is missing
```
Ejemplo:
```
no_document_found
```
### `401`
No se logró autenticar. El cuerpo viene vacío; revisa tu web ID, que la llave de API esté
activa, y que la cadena firmada siga las reglas del esquema `signmage`.
# Descargar el historial de un documento en PDF (https://devs.firmame.com/docs/api/consultas/get-document-history-pdf)
`GET https://app.firmame.com/api/v1/document/{frmxId}/history/pdf`
Requiere el header `Authorization` del esquema `signmage`.
El mismo historial que
[`GET /api/v1/document/{frmxId}/history/json`](/docs/api/consultas/get-document-history), pero
como PDF listo para entregárselo a alguien. Es el archivo que Firmame adjunta al correo de flujo
completado, y el que un paquete de verificación NOM-151 espera como historial.
La respuesta es el PDF mismo, no base64 dentro de un JSON: lee los bytes y escríbelos a un
archivo. Llega como `Content-disposition: attachment; filename=historial.pdf`.
Usa [la versión en JSON](/docs/api/consultas/get-document-history) cuando lo que quieras sea
mover tu propia interfaz con los eventos — ahí está también el catálogo de tipos de evento.
## Parámetros
| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
| `frmxId` | path | string | requerido | El id del documento. |
## Respuestas
### `200`
El historial de eventos como PDF.
`application/pdf` — `string`.
### `401`
No se logró autenticar. El cuerpo viene vacío; revisa tu web ID, que la llave de API esté
activa, y que la cadena firmada siga las reglas del esquema `signmage`.
### `500`
No se pudo generar el historial. El cuerpo viene vacío.
# Obtener el historial de un documento (https://devs.firmame.com/docs/api/consultas/get-document-history)
`GET https://app.firmame.com/api/v1/document/{frmxId}/history/json`
Requiere el header `Authorization` del esquema `signmage`.
El historial de un documento contiene información de la generación de un documento, envío,
seguimiento y firmas. Este endpoint te lo devuelve como un arreglo de eventos, del más reciente
al más viejo.
Sirve para mostrar el avance en tu propia interfaz sin esperar a los webhooks. Si lo quieres
como PDF para entregárselo a alguien, usa
[`GET /api/v1/document/{frmxId}/history/pdf`](/docs/api/consultas/get-document-history-pdf).
### Los eventos
| `documentEventType` | Significado |
|---|---|
| `DOCUMENT_CREATED` | Se creó el documento |
| `SIGNATURE_INVITATION` | Se envió una invitación para firma |
| `NON_SIGNER_RECIPENT_FLOW_STARTED` | Se notificó a un destinatario no firmante |
| `FLOW_STARTED` | Se inició el proceso de firma |
| `FLOW_EXPIRED` | El proceso de firma llegó a su fecha de expiración |
| `DOCUMENT_SIGNED` | Se firmó el documento |
| `DOCUMENT_STAMPED` | Se selló el documento |
| `DOCUMENT_COMPLETED` | Se completó el documento |
| `REJECTED_DOCUMENT` | Un firmante rechazó el documento |
| `DOCUMENT_BLOCKED_FOR_SIGNATURE` | Se bloqueó el documento para firma |
| `DOCUMENT_UNBLOCKED_FOR_SIGNATURE` | Se desbloqueó el documento para firma |
| `DOCUMENT_FLATTENED` | Se aplanaron los campos de formulario del documento |
| `DOCUMENT_TRANSFERRED` | Se transfirió el documento a otro usuario |
| `IDENTITY_VERIFICATION_PASSED` | Un firmante pasó la verificación de identidad |
| `IDENTITY_VERIFICATION_FAILED` | Un firmante no pasó la verificación de identidad |
| `EMAIL_DELIVERED` | Se entregó un correo |
| `EMAIL_OPENED` | El destinatario abrió el correo |
| `EMAIL_CLICKED` | El destinatario hizo clic en el correo |
| `EMAIL_FAILED` | Falló una entrega de correo |
| `EMAIL_COMPLAINED` | El destinatario reportó el correo como spam |
| `WHATSAPP_SENT` | Se envió un mensaje de WhatsApp |
| `WHATSAPP_DELIVERED` | Se entregó un mensaje de WhatsApp |
| `WHATSAPP_READ` | Se leyó un mensaje de WhatsApp |
Compara contra estas cadenas tal como están escritas, `NON_SIGNER_RECIPENT_FLOW_STARTED`
incluida.
### Qué trae `jsonMetaData`
`jsonMetaData` es una **cadena con JSON dentro**, no un objeto JSON: parséala antes de leerla.
Sus llaves dependen del evento, y viene en `null` para los eventos que no llevan nada extra.
| Evento | Llaves |
|---|---|
| `DOCUMENT_CREATED` | `createdBy` |
| `SIGNATURE_INVITATION` y los demás eventos de invitación | `sentTo` |
| `FLOW_STARTED` | `ordered`, `remindEvery`, `lastSignatureCount`, `expirationDate`, `lockOnExpiration` |
| `FLOW_EXPIRED` | `expirationDate`, `lockOnExpiration` |
| `DOCUMENT_SIGNED` | `signer`, `ipAddress`, y `boxData` / `boxContactData` cuando el sticker los trae |
| `REJECTED_DOCUMENT` | `rejector`, `rejectReason` |
| `DOCUMENT_UNBLOCKED_FOR_SIGNATURE` | `comments` |
| `DOCUMENT_TRANSFERRED` | `originalOwnerUserId`, `newOwnerUserId` |
| `IDENTITY_VERIFICATION_PASSED` / `_FAILED` | `signerData`, `workflowId`, y `firstName`, `lastName`, `curp` cuando se leyó la identificación |
| `EMAIL_*` | `threadId`, `recipient` |
El `threadId` de los eventos de correo es el mismo id que le pasas a los endpoints de correos
certificados, así que puedes pasar de «este correo se abrió» al mensaje mismo.
## Parámetros
| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
| `frmxId` | path | string | requerido | El id del documento. |
## Respuestas
### `200`
Los eventos del documento, del más reciente al más viejo.
`application/json`:
- `documentEventType` (string, opcional) — Cuál evento es. Ve la tabla de arriba.
- `jsonMetaData` (string | null, opcional) — Detalle extra como cadena JSON, o `null`. Las llaves dependen del evento.
- `eventDate` (integer, opcional) — Cuándo ocurrió el evento, en milisegundos del epoch.
- `createdAt` (integer, opcional) — Cuándo se registró el evento, en milisegundos del epoch.
Ejemplo:
```json
[
{
"documentEventType": "DOCUMENT_STAMPED",
"jsonMetaData": null,
"eventDate": 1711128805000,
"createdAt": 1711128805000
},
{
"documentEventType": "EMAIL_OPENED",
"jsonMetaData": "{\"threadId\":\"<20240322175325.1.abc@mg.firmamex.com>\",\"recipient\":\"firmante@dominio.com\"}",
"eventDate": 1711128512000,
"createdAt": 1711128512000
},
{
"documentEventType": "DOCUMENT_CREATED",
"jsonMetaData": "{\"createdBy\":\"tu-cuenta@dominio.com\"}",
"eventDate": 1711128400000,
"createdAt": 1711128400000
}
]
```
### `401`
No se logró autenticar. El cuerpo viene vacío; revisa tu web ID, que la llave de API esté
activa, y que la cadena firmada siga las reglas del esquema `signmage`.
### `422`
No hay ningún documento con ese id en esta cuenta.
# Obtener el reporte de firmas de un documento (https://devs.firmame.com/docs/api/consultas/get-document-report)
`GET https://app.firmame.com/api/report/{ticket}`
Requiere el header `Authorization` del esquema `signmage`.
Devuelve el reporte forense completo del documento: cada firma con su posición, su cadena de
certificados, su estado OCSP, su autoridad de sellado de tiempo, y si sigue siendo válida.
Este es el endpoint para verificar un documento por fuera, no para ver el avance — para el
avance usa `GET /api/document` o los webhooks.
## Parámetros
| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
| `ticket` | path | string | requerido | El id del documento (`firmamexId` / `document_ticket`). |
## Respuestas
### `200`
El reporte, con una entrada en `firmas` por cada firma encontrada en el documento.
Las fechas dentro del reporte son cadenas con formato, no milisegundos del epoch — a
diferencia del resto de esta API.
Varios campos de una entrada son condicionales y simplemente no vienen cuando no
aplican, así que revisa si la llave existe en vez de esperar un `null`.
`application/json`:
- `firmas` (array, opcional) — Una entrada por firma digital.
- `page` (integer, opcional)
Página donde está el recuadro de la firma, contando desde 1 — el `page` de un sticker
cuenta desde 0, así que difieren en uno. `-1` cuando la firma no tiene recuadro.
- `left` (number, opcional) — Distancia al borde izquierdo, en unidades PDF.
- `top` (number, opcional) — Distancia del borde inferior a la parte superior del recuadro, en unidades PDF.
- `width` (number, opcional)
- `height` (number, opcional)
- `campoFirma` (string, opcional) — Nombre del campo de firma del PDF.
- `signatureType` (string, opcional, `nom151` | `universal` | `original`) — A cuál representación pertenece esta firma.
- `validSignature` (boolean, opcional) — Si la firma verifica contra los bytes que cubre.
- `coversDocument` (boolean, opcional)
Si la firma cubre el documento completo. `false` significa que se agregó contenido
después, lo normal en todas las firmas menos la última.
- `downstream` (boolean, opcional) — Si la firma se recolectó a través de una autoridad de Firmame.
- `signDate` (string, opcional) — Cuándo se aplicó la firma, como `EEE MMM dd HH:mm:ss zzz yyyy` — no es ISO 8601.
- `totalRevisions` (integer, opcional) — Siempre `0`.
- `reason` (string, opcional) — La razón registrada en la firma, cuando trae una.
- `certType` (string, opcional, `EMAIL` | `IDENTITY` | `NONE`)
El tipo de confianza detrás del certificado de firma. `NONE` significa que la autoridad
emisora no es una que Firmame reconozca.
- `signCertificateSerial` (string, opcional) — Número de serie del certificado de firma, en hexadecimal y minúsculas.
- `box` (integer, opcional, format `int64`) — Id del sticker que llenó esta firma. Solo aparece en las firmas recolectadas por Firmame.
- `email` (string, opcional) — El correo del firmante, cuando el certificado o la autoridad lo traen.
- `rfc` (string, opcional) — El RFC del firmante, cuando el certificado trae un UID de menos de 20 caracteres.
- `uid` (string, opcional) — El UID del certificado, cuando tiene 20 caracteres o más.
- `ipAddress` (string, opcional) — Dirección desde la que se aplicó la firma, cuando quedó registrada.
- `latLng` (string, opcional) — Coordenadas desde las que se aplicó la firma, cuando quedaron registradas.
- `timestampAuthority` (string, opcional) — Nombre distintivo de la autoridad de sellado de tiempo, cuando la firma trae sello.
- `downstreamData` (object, opcional) — Cómo se identificó el firmante, en las firmas recolectadas a través de Firmame.
- `authority` (string, opcional)
- `dataType` (string, opcional)
- `subject` (string, opcional)
- `certImage` (string, opcional)
- `ocsp` (object, opcional) — La consulta de revocación; solo aparece cuando la firma trae una respuesta OCSP dentro.
- `signatureValid` (boolean, opcional) — Siempre `true` cuando este objeto está presente.
- `certStatus` (string, opcional)
`good` cuando el certificado era válido al momento de firmar. Cualquier otro valor
significa que no lo era, y no es una cadena estable con la que puedas comparar.
- `signer` (string, opcional) — Nombre común del respondedor que contestó la consulta.
- `certificados` (array, opcional) — La cadena de certificados de la firma. Siempre viene, aunque puede ir vacía.
- `commonName` (string, opcional)
- `email` (string, opcional)
- `organization` (string, opcional)
- `location` (string, opcional)
- `country` (string, opcional)
- `serial` (string, opcional) — Número de serie, en hexadecimal.
- `signatureAlgorithm` (string, opcional)
- `expirationDate` (string, opcional)
- `validationDate` (string, opcional)
- `issuerCommonName` (string, opcional)
- `issuerEmail` (string, opcional)
- `issuerOrganization` (string, opcional)
- `issuerLocation` (string, opcional)
- `issuerCountry` (string, opcional)
- `publicKey` (string, opcional)
- `hexCertificado` (string, opcional) — El certificado completo, DER, en hexadecimal y mayúsculas.
- `InkSignatures` (array