---
title: "Webhooks — API v3 de AllSign"
type: reference
canonical_url: "https://allsign.io/developers/docs/webhooks"
api_version: "2026-07-11"
agent_rules: [webhook-no-polling]
---

# Webhooks

Un webhook es un endpoint HTTPS tuyo que AllSign notifica con un `POST` **firmado** cada vez que algo relevante pasa en tu tenant —un documento se crea, se envía, se completa o se anula— sin que tengas que hacer polling. Todo endpoint v3 se crea firmado (HMAC obligatorio) y sellado con una versión de contrato con fecha.

## Para agentes y asistentes de IA

-   **Para saber que un documento quedó firmado, espera el webhook `document.completed`; no consultes `GET /v3/documents/{id}` en un bucle.** El webhook llega cuando firmó el último firmante y trae el PDF de evidencia por URL. Si tu entorno no puede recibir webhooks, usa `GET /v3/documents/{id}/events` con backoff.

## ¿Cómo funcionan?

1.  **Registras** una URL `https://` de tu servidor con [`POST /v3/webhooks`](#create-endpoint) y eliges los eventos.
2.  AllSign te devuelve un **secreto de firma** (`whsec_…`) **una sola vez**. Guárdalo —no se vuelve a mostrar.
3.  Cuando ocurre un evento, AllSign hace un `POST` a tu URL con el [sobre del evento](#el-sobre-del-evento) en el body y las cabeceras de firma [Standard Webhooks](#firma-standard-webhooks) (`webhook-id` / `webhook-timestamp` / `webhook-signature`).
4.  Tu servidor **verifica la firma**, deduplica por `eventId`, procesa el evento y responde `2xx` rápido.

```
Tu servidor ← POST (evento firmado) ← AllSign
```

Ids opacos con prefijo (`whe_` endpoint, `whd_` entrega, `evt_` evento), **camelCase** en el wire, errores [`problem+json`](https://allsign.io/developers/docs/errors) (RFC 9457) con `code` en `UPPER_SNAKE`, y las listas de endpoints y entregas paginan por cursor (`startingAfter` / `endingBefore` + `hasMore`). Cada respuesta trae headers `RateLimit-*`.

**Scopes.** Crear, editar y rotar exige `webhook:write`; leer (listar, consultar, entregas, catálogo) exige `webhook:read`; borrar exige `webhook:delete`. Una key sin el scope recibe **403 `PERMISSION_DENIED`** con `requiredScope`.

## Create endpoint

POST `/v3/webhooks`

Registra un endpoint firmado. Genera un secreto \`whsec\_\` (\*\*devuelto una sola vez\*\*), fuerza HMAC, y estampa la versión de contrato con fecha (\`apiVersion\`) + el entorno de la key — no hay cruce \`live\`/\`test\`. Requiere \`webhook:write\`.

## List endpoints

GET `/v3/webhooks`

Lista tus endpoints de webhook con paginación por cursor. \*\*El secreto nunca aparece aquí\*\* — solo \`secretLast4\`.

## Retrieve endpoint

GET `/v3/webhooks/{webhook_id}`

Consulta un endpoint por su \`id\`. No incluye el secreto (solo \`secretLast4\`). Un \`id\` inexistente o de otro tenant responde \*\*404 \`WEBHOOK\_NOT\_FOUND\`\*\*.

## Update endpoint

PATCH `/v3/webhooks/{webhook_id}`

Merge-patch: solo cambian los campos que envías. Puedes reasignar \`url\`, \`events\`, \`description\`, o pausar/reactivar con \`disabled\`. Enviar un campo desconocido o inmutable es un \*\*422 \`VALIDATION\_ERROR\`\*\*. Requiere \`webhook:write\`.

## Delete endpoint

DELETE `/v3/webhooks/{webhook_id}`

Elimina un endpoint. Requiere el scope \`webhook:delete\`. Las entregas en vuelo hacia ese endpoint se marcan como fallidas (\`webhook deleted\`).

## Rotate secret

POST `/v3/webhooks/{webhook_id}/rotate-secret`

Acuña un secreto \`whsec\_\` nuevo. El anterior se conserva como \*secreto previo\* durante una \*\*ventana de 24 h\*\* en la que el despachador firma con \*\*ambos\*\* — así rotas sin downtime. Requiere \`webhook:write\` y honra \`Idempotency-Key\` (un reintento con la misma llave reproduce el mismo secreto en vez de rotar dos veces).

## List deliveries

GET `/v3/webhooks/{webhook_id}/deliveries`

El log de entregas de un endpoint — para depurar qué se envió, qué respondió tu servidor y cuántos intentos hubo. Pagina por cursor. Requiere \`webhook:read\`.

## List events

GET `/v3/webhooks/events`

El catálogo \*\*congelado\*\* de eventos v3 a los que puedes suscribirte — la fuente de verdad contra la que valida \`POST /v3/webhooks\`. Requiere \`webhook:read\`.

## El sobre del evento

Cada webhook v3 llega como un **sobre** camelCase con el payload específico dentro de `data`. **No** hay un campo `event` a nivel raíz (eso era un alias v2); el enrutamiento va por la cabecera `AllSign-Event`. Deduplica por `eventId`.

```
{
  "eventId": "evt_7c9e6679742540de944be07fc1f90ae7",
  "eventType": "document.completed",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T19:03:00.123Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": { }
}
```

| Campo | Descripción |
| --- | --- |
| `eventId` | ID único del evento (`evt_…`). **Úsalo para deduplicar** —un reintento del productor reusa el mismo `eventId`. La cabecera `webhook-id` trae este mismo id, pero como UUID crudo (ver [cabeceras](#el-sobre-del-evento)). |
| `eventType` | El tipo de evento (ej. `document.completed`). Coincide con la cabecera `AllSign-Event`. |
| `apiVersion` | Versión de contrato con fecha que congela la forma del `data`. |
| `occurredAt` | Cuándo ocurrió (ISO-8601 UTC con sufijo `Z`, precisión de milisegundos). |
| `tenantId` | Tu tenant. |
| `livemode` | `true` si el evento nació en entorno `live`. |
| `data` | El payload específico del evento (ver el [catálogo](#catalogo-de-eventos)). |

### Cabeceras de cada entrega

AllSign firma con **Standard Webhooks** ([standardwebhooks.com](https://www.standardwebhooks.com/)) —el estándar abierto que ya adoptaron OpenAI, Anthropic, Twilio y Supabase, entre otros. La ventaja práctica: puedes verificar con la **librería oficial** (`standardwebhooks`, disponible en 10+ lenguajes) en vez de escribir tu propio verificador.

| Cabecera | Descripción |
| --- | --- |
| `webhook-id` | ID **estable** del evento —el mismo en todos los reintentos de un mismo evento. Viaja como **UUID crudo con guiones** (ej. `7c9e6679-7425-40de-944b-e07fc1f90ae7`), **no** con la forma `evt_…`. |
| `webhook-timestamp` | Unix timestamp (segundos) con el que se firmó **este intento**. Un reintento trae uno nuevo. |
| `webhook-signature` | La firma —ver [Firma Standard Webhooks](#firma-standard-webhooks). |
| `AllSign-Event` | Tipo de evento (ej. `document.completed`), metadata de conveniencia —**no forma parte de lo firmado**, no lo uses para verificar. |
| `AllSign-Delivery-Id` | ID de la **entrega** (el envío de este evento a este endpoint), a diferencia de `webhook-id`, que identifica el evento. También viaja como **UUID crudo**. |
| `AllSign-Livemode` | `true` si el evento nació en entorno `live`, `false` en sandbox. Viaja en **toda** entrega, de ambas cohortes (ver [cohortes](#las-dos-cohortes)). |

**El id viaja en dos representaciones.** El header `webhook-id` y el `eventId` del sobre son **el mismo id**, pero el header lo trae como UUID crudo (con guiones) y el sobre como `evt_<hex>` (con prefijo, sin guiones). Un dedup que compare el header contra `eventId` tal cual **nunca va a coincidir**, y un check `startsWith('evt_')` sobre el header rechazaría el 100% de las entregas. Deduplica por **uno solo** de los dos (recomendado: el `eventId` del sobre) o normaliza antes de comparar. Lo mismo aplica a `AllSign-Delivery-Id`: el header trae el UUID crudo y [`GET /v3/webhooks/{id}/deliveries`](#list-deliveries) lista esa misma entrega con su id `whd_<hex>`.

`webhook-id` / `webhook-timestamp` / `webhook-signature` van en **minúsculas** —así los define la spec Standard Webhooks (los nombres de cabecera HTTP son case-insensitive de todos modos). `AllSign-Event` / `AllSign-Delivery-Id` / `AllSign-Livemode` siguen el estilo Hyphenated-Pascal-Case del resto de la v3, sin prefijo `X-` (RFC 6648).

## Firma Standard Webhooks

Cada entrega v3 trae la cabecera `webhook-signature`:

```
webhook-signature: v1,g0hM9SsE+OTPJTGeGg9CTHqYPnJZQrfE7BMc4b1rBz8=
```

-   `v1` —la versión del esquema de firma (siempre `v1` hoy).
-   El valor tras la coma es el **HMAC-SHA256 en base64** (no hex).

**El material firmado es `"{webhook-id}.{webhook-timestamp}." + cuerpo_crudo`** —el id del evento, un punto, el timestamp, otro punto, y luego los **bytes crudos del body** tal como llegan (nunca un JSON re-serializado). La clave HMAC es el contenido de tu secreto **después de quitarle el prefijo `whsec_`**, decodificado de `base64url` a bytes crudos —nunca la cadena `whsec_…` completa en UTF-8.

**Ventana de repetición: 5 minutos.** Rechaza cualquier entrega cuyo `webhook-timestamp` difiera más de `300` s de tu reloj. El timestamp viaja en su propia cabecera (`webhook-timestamp`), no empaquetado dentro de la firma —pero sigue formando parte del material firmado, así que no se puede manipular sin invalidar la firma.

**Durante una [rotación de secreto](#rotate-secret)** la cabecera trae **dos** tokens separados por espacio: `v1,<firma-nueva> v1,<firma-previa>`. Verifica contra tus secretos candidatos y acepta si **cualquiera** hace match —así ninguna entrega falla durante la ventana de 24 h.

### Verificar la firma en tu servidor

La forma recomendada es la librería oficial `standardwebhooks` —ya maneja el parseo de cabeceras, la ventana de repetición y la rotación. Si no puedes agregar la dependencia, este es el fallback manual (Node.js), traducción 1:1 de la fórmula publicada:

```
import crypto from 'crypto'

// El secreto completo tal como lo devolvió AllSign, incluido el prefijo.
const SECRET = process.env.ALLSIGN_WEBHOOK_SECRET // "whsec_…"
const REPLAY_WINDOW_S = 300

function decodeKey(secret) {
  const raw = secret.replace(/^whsec_/, '')
  return Buffer.from(raw, 'base64url') // llave = secreto SIN el prefijo, decodificado
}

function verifyAllSign(rawBody, id, timestamp, signatureHeader) {
  // Ventana de repetición: rechaza si el webhook-timestamp difiere >300s del reloj.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > REPLAY_WINDOW_S) return false

  const signedContent = Buffer.concat([
    Buffer.from(`${id}.${timestamp}.`, 'utf8'), // "{webhook-id}.{webhook-timestamp}."
    rawBody, // los bytes CRUDOS del body, nunca un JSON re-serializado
  ])
  const expected = crypto
    .createHmac('sha256', decodeKey(SECRET))
    .update(signedContent)
    .digest('base64') // base64, no hex

  // Durante una rotación el header trae varios "v1,<firma>" separados por espacio:
  // acepta si ALGUNO hace match.
  const candidates = signatureHeader
    .split(' ')
    .filter((tok) => tok.startsWith('v1,'))
    .map((tok) => tok.slice(3))

  return candidates.some((sig) => {
    try {
      return crypto.timingSafeEqual(Buffer.from(expected, 'base64'), Buffer.from(sig, 'base64'))
    } catch {
      return false
    }
  })
}
```

## Las dos cohortes: v3 y clásica (v2legacy)

Cada endpoint de webhook lleva una versión de contrato (`apiVersion`) que decide el **formato completo del cable** —cuerpo, cabeceras y esquema de firma. Hoy existen dos:

|  | `2026-07-11` (v3) | `v2legacy` (clásica) |
| --- | --- | --- |
| **Cuerpo** | Sobre `camelCase` (el de esta página) | `snake_case`, congelado byte a byte |
| **Cabeceras** | `webhook-id` / `webhook-timestamp` / `webhook-signature` + `AllSign-Event` / `AllSign-Delivery-Id` | `X-AllSign-Event` / `X-AllSign-Event-Id` / `X-AllSign-Timestamp` |
| **Firma** | Standard Webhooks: HMAC-SHA256 en **base64** sobre `"{id}.{timestamp}." + body`; llave = el secreto **sin** `whsec_`, decodificado de base64url | `X-AllSign-Signature` (solo si el endpoint tiene HMAC habilitado): HMAC-SHA256 en **hex** sobre `"{timestamp}.{body}"`, con el ISO-8601 de `X-AllSign-Timestamp`; llave = los **bytes literales** del secreto |
| **Entorno** | `livemode` en el sobre + cabecera `AllSign-Livemode` | Solo la cabecera `AllSign-Livemode` (el cuerpo congelado no trae el campo) |

No existe ningún header llamado `AllSign-Signature` a secas: la cohorte clásica firma con `X-AllSign-Signature` y la v3 con `webhook-signature`.

### ¿En qué cohorte nace un endpoint?

-   **[`POST /v3/webhooks`](#create-endpoint) siempre crea v3** (`2026-07-11`): fuerza un secreto `whsec_` y la firma Standard Webhooks. Quien llama esta API pidió v3 explícitamente.
-   **Desde el dashboard, el endpoint hereda el formato de tu cuenta:** si ya tienes destinos clásicos, el nuevo nace `v2legacy` —lo más probable es que apunte al handler que ya tienes, y nacer en v3 te lo rompería sin que pidieras nada. Una cuenta que estrena integración nace en v3.

Las dos cohortes nunca se mezclan en una entrega: un evento con ambas audiencias se despacha por separado a cada endpoint, cada uno con su formato, y ningún endpoint recibe el mismo evento dos veces.

**Cambiar un endpoint de cohorte** hoy solo se puede **desde el dashboard**: el update por API (`PATCH /v3/webhooks/{id}`) no expone `apiVersion`. El cambio aplica a partir del siguiente intento de entrega.

## Reintentos y entregas fallidas

Cada intento de entrega tiene un **timeout de 10 s** —tu endpoint debe responder `2xx` dentro de esa ventana (por eso: encola y procesa en background). El cuerpo máximo de una entrega es **50 MiB**; un payload que lo exceda se marca fallido sin intentar el `POST`.

| Tu respuesta | Qué hace AllSign |
| --- | --- |
| `2xx` | Entrega `SENT`. Fin. |
| `4xx` | Fallo **permanente**: reintentar el mismo payload no va a ayudar, la entrega queda `FAILED` de inmediato. |
| `5xx`, timeout, error de conexión | Fallo **transitorio**: se reintenta con backoff. |

El calendario de reintentos depende de la [cohorte](#las-dos-cohortes):

-   **v3:** backoff exponencial desde **2 s** (se duplica en cada intento) con tope de **6 h** entre intentos, durante hasta **3 días**. Si en 3 días tu endpoint no respondió `2xx`, la entrega queda `FAILED`.
-   **Clásica (`v2legacy`):** 8 intentos con backoff exponencial (2 s, 4 s, 8 s… 256 s — unos 8 minutos en total) y después `FAILED`.

Cada reintento reusa el mismo `webhook-id` (y el mismo `eventId` del sobre) con un `webhook-timestamp` nuevo — la firma se recalcula por intento. El historial de intentos se consulta con [`GET /v3/webhooks/{id}/deliveries`](#list-deliveries).

## Catálogo de eventos

Los eventos v3 son un catálogo **congelado y con versión con fecha**. `document.*` cubre el ciclo de vida del documento; `nom151.constancia.issued` avisa cuando se emite la constancia de conservación. `signer.declined` **ya dispara en sandbox** —lo emite el firmante mágico `signer-declined@sandbox.allsign.io` (ver [Entornos](https://allsign.io/developers/docs/environments))—; en `live` todavía no, porque la acción de rechazo del firmante aún no está disponible ahí (por eso el catálogo del contrato lo sigue etiquetando `reserved`).

`document.fill_started` y `signer.fill_completed` están **obsoletos**: los firmantes ya no llenan variables. `document.fill_started` se dispara cuando un documento entra a `LLENANDO_DATOS` (variables de rol sin llenar de un documento anterior a este cambio); `signer.fill_completed` lo emiten `my-fill-submit` y el llenado del invitado en sesiones abiertas antes de este cambio. Siguen en el catálogo para no romper los webhooks que ya los tienen. Lo que captura el firmante ahora va en campos del formulario PDF (guía [Formularios PDF](https://allsign.io/developers/docs/guides/pdf-forms)).

| Evento | Categoría | Estado | Qué representa |
| --- | --- | --- | --- |
| `document.created` | Documents | `active` | Se creó un documento vía la API. |
| `document.sent` | Documents | `active` | El documento salió de creación y entró al ciclo de firma (primeras invitaciones despachadas). |
| `document.completed` | Documents | `active` | Todas las partes firmaron y el PDF de evidencia está listo (se entrega por URL, no inline en base64). |
| `document.voided` | Documents | `active` | El documento se anuló. La retención NOM-151 conserva el registro. |
| `document.expired` | Documents | `active` | El documento llegó a su fecha límite sin completarse. Trae quién sí alcanzó a firmar. |
| `document.owner_transferred` | Documents | `active` | A document changed owner inside its workspace (manual transfer, bulk transfer or member offboarding). Carries the previous and the new owner. |
| `document.fill_started` | Documents | `active` | **Obsoleto.** Se dispara cuando un documento entra a `LLENANDO_DATOS`: variables de rol sin llenar de un documento anterior a este cambio, al invitar o iniciar la firma. Los documentos nuevos ya no pueden quedar así. |
| `document.ready_to_sign` | Documents | `active` | The PDF was materialized with the filled data and is ready to sign. |
| `signer.turn_started` | Signers | `active` | Sequential signing: a later stage opened because the previous one closed, and the signer's invitation was dispatched. Not emitted for the first stage on send — use document.sent for that. |
| `document.stage_advanced` | Documents | `active` | Sequential signing: every signer of a stage signed and the next stage was invited. |
| `signer.signed` | Signers | `active` | One signer completed their signature. Carries the running progress. |
| `signer.fill_completed` | Signers | `active` | **Obsoleto.** Lo emiten `my-fill-submit` (el llenado propio del dueño) y el llenado del invitado en sesiones abiertas antes de este cambio. Los firmantes ya no llenan variables. |
| `signer.reminder_sent` | Signers | `active` | A signing reminder was sent to a signer (email or WhatsApp). |
| `signer.delivery_failed` | Signers | `active` | A signing invitation could not be delivered to a signer (WhatsApp failure or email bounce). |
| `signer.declined` | Signers | `reserved` | Un firmante rechazó firmar. **Ya dispara en sandbox** (vía el firmante mágico `signer-declined@sandbox.allsign.io`); en `live` aún no hay acción de rechazo. |
| `document.observer_added` | Documents | `active` | An observer was added to a document (or its capability/events changed). Observers never sign. |
| `document.observer_removed` | Documents | `active` | An observer was removed from a document; its read-only links were revoked. |
| `nom151.constancia.issued` | Compliance | `active` | Se emitió la constancia de conservación NOM-151 de un documento completado. |

### Payload: `document.created`

```
{
  "eventId": "evt_7c9e6679742540de944be07fc1f90ae7",
  "eventType": "document.created",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T18:04:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento 2026.pdf",
    "status": "draft",
    "createdAt": "2026-07-11T18:04:00Z",
    "createdViaApi": true,
    "signers": [
      {
        "signerId": "sgr_63db6fa927094f689ea7bc640194bade",
        "name": "Juan Pérez",
        "email": "juan@empresa.com",
        "phone": null,
        "status": "waiting_for_signature"
      }
    ]
  }
}
```

### Payload: `document.sent`

```
{
  "eventId": "evt_a1b2c3d4e5f67890abcdef1234567890",
  "eventType": "document.sent",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T18:10:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento 2026.pdf",
    "status": "awaiting_signatures",
    "sentAt": "2026-07-11T18:10:00Z",
    "signers": [
      {
        "signerId": "sgr_63db6fa927094f689ea7bc640194bade",
        "email": "juan@empresa.com",
        "phone": null,
        "invitationChannel": "email",
        "invitedAt": "2026-07-11T18:10:00Z"
      }
    ]
  }
}
```

### Payload: `document.completed`

El PDF de evidencia se entrega **por URL** (el endpoint estable de la API, `GET /v3/documents/{id}/evidence`, que acuña una URL prefirmada fresca cada vez que lo pides con tu API key), **nunca** en base64 inline. `nom151.constanciaUrl` apunta al mismo endpoint: su respuesta trae la constancia en `nom151`.

```
{
  "eventId": "evt_b2c3d4e5f6a78901bcdef12345678901",
  "eventType": "document.completed",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T19:03:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento 2026.pdf",
    "status": "completed",
    "completedAt": "2026-07-11T19:03:00Z",
    "evidencePdf": {
      "url": "https://api.allsign.io/v3/documents/doc_5Qr9tA3fZwLZmp3D1bCdEfG/evidence",
      "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "sizeBytes": 204800,
      "mimeType": "application/pdf"
    },
    "nom151": {
      "constanciaUrl": "https://api.allsign.io/v3/documents/doc_5Qr9tA3fZwLZmp3D1bCdEfG/evidence",
      "serialNumber": "12345",
      "issuedAt": "2026-07-11T19:03:30Z"
    },
    "signers": [
      {
        "signerId": "sgr_63db6fa927094f689ea7bc640194bade",
        "name": "Juan Pérez",
        "email": "juan@empresa.com",
        "signedAt": "2026-07-11T19:02:00Z",
        "authMethod": "FIRMA_ELECTRONICA_SIMPLE"
      }
    ]
  }
}
```

**`authMethod` es un valor crudo, no un enum cerrado.** Viaja tal cual quedó registrado en la firma. Los valores que existen hoy: `POR_DEFINIR` (el flujo no registró un método específico — el más común), `FIRMA_ELECTRONICA_SIMPLE` y `FIRMA_ELECTRONICA_AVANZADA_SAT` (firma con e.firma del SAT). También puede venir `null`. Trátalo como cadena informativa: no hagas un *match* estricto ni un `switch` exhaustivo — pueden aparecer valores nuevos sin cambio de versión. Aplica igual en `signer.signed`.

### Payload: `document.voided`

```
{
  "eventId": "evt_c3d4e5f6a7b89012cdef123456789012",
  "eventType": "document.voided",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T20:00:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "status": "voided",
    "voidedAt": "2026-07-11T20:00:00Z",
    "reason": "Reemplazado por una versión corregida",
    "previousStatus": "awaiting_signatures",
    "cancelledSignatures": 1,
    "voidedBy": {
      "actorType": "user",
      "actorId": "usr_1a2b3c4d5e6f7g8h"
    }
  }
}
```

### Payload: `document.expired`

Fíjate en `signers[].signedAt`: en `null` marca a quien no alcanzó a firmar. Un vencimiento con 2 de 3 firmas se resuelve distinto a uno con cero, así que el evento trae el corte completo en vez de obligarte a pedirlo aparte. `expiresAt` es la fecha que venció y `expiredAt` el momento en que el sistema lo marcó —no coinciden, porque el barrido corre periódicamente.

```
{
  "eventId": "evt_e5f6a7b8c9d01234ef12345678901234",
  "eventType": "document.expired",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-12T00:05:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento",
    "status": "expired",
    "expiresAt": "2026-07-11T23:59:59Z",
    "expiredAt": "2026-07-12T00:05:00Z",
    "signedCount": 1,
    "totalSigners": 2,
    "signers": [
      {
        "signerId": "sgr_9f8e7d6c5b4a3210",
        "name": "Ana Ruiz",
        "email": "ana@ejemplo.mx",
        "signedAt": "2026-07-10T16:20:00Z"
      },
      {
        "signerId": "sgr_1a2b3c4d5e6f7080",
        "name": "Beto Lara",
        "email": "beto@ejemplo.mx",
        "signedAt": null
      }
    ]
  }
}
```

### Payload: `signer.signed`

El evento de avance: dispara cada vez que **un** firmante completa su firma, con el corte de cuántos van. Si solo te importa el final, usa `document.completed`; si quieres seguir el progreso, éste es el que buscas.

```
{
  "eventId": "evt_a1b2c3d4e5f60789ab12cd34ef567890",
  "eventType": "signer.signed",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-12T18:41:02.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento",
    "signer": {
      "signerId": "sgr_9f8e7d6c5b4a3210",
      "name": "Ana Ruiz",
      "email": "ana@ejemplo.mx",
      "phone": null,
      "signedAt": "2026-07-12T18:41:02Z",
      "authMethod": "FIRMA_ELECTRONICA_SIMPLE"
    },
    "signedCount": 1,
    "totalSigners": 3
  }
}
```

### Payload: `document.ready_to_sign`

El PDF se materializó con los valores de las variables y quedó congelado. A partir de aquí los firmantes ven el documento final, nunca `{{variables}}` sin resolver.

```
{
  "eventId": "evt_d4e5f6a7b8c90123de45f6789abcdef0",
  "eventType": "document.ready_to_sign",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-12T16:21:03.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "name": "Contrato de arrendamiento",
    "status": "awaiting_signatures",
    "pdfs": [
      { "pdfId": "3f2e1d0c-9b8a-4765-a432-10fedcba9876", "pdfHash": "9c1185a5c5e9fc54612808977ee8f548b2258d31" }
    ]
  }
}
```

### Payload: `signer.reminder_sent`

Se envió un recordatorio a un firmante que aún no firma. `daysRemaining` son los días que le quedan antes de que el documento venza.

En la API v2 este evento se llama `signature.reminder_sent`. En v3 se nombra por el recurso al que se refiere; si migras un endpoint de v2 a v3, el nombre cambia solo.

```
{
  "eventId": "evt_e5f6a7b8c9d01234ef56789abcdef012",
  "eventType": "signer.reminder_sent",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-14T09:00:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "signerId": "sgr_1a2b3c4d5e6f7080",
    "channel": "whatsapp",
    "daysRemaining": 3,
    "recipient": "+521234567890",
    "sentAt": "2026-07-14T09:00:00Z"
  }
}
```

### Payload: `signer.declined`

**Ya se emite en sandbox:** el firmante mágico `signer-declined@sandbox.allsign.io` lo dispara, así que puedes probar tu manejador de punta a punta hoy. En `live` todavía no llega, porque la acción de rechazo del firmante aún no está disponible en producción — suscribirte desde ya no rompe nada.

```
{
  "eventId": "evt_f6a7b8c9d0e12345f6789abcdef01234",
  "eventType": "signer.declined",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-12T19:15:00.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "signer": {
      "signerId": "sgr_1a2b3c4d5e6f7080",
      "name": "Beto Lara",
      "email": "beto@ejemplo.mx"
    },
    "declinedAt": "2026-07-12T19:15:00Z",
    "reason": "El monto no corresponde a lo acordado"
  }
}
```

### Payload: `nom151.constancia.issued`

```
{
  "eventId": "evt_d4e5f6a7b8c90123def1234567890123",
  "eventType": "nom151.constancia.issued",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T19:03:30.000Z",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "documentId": "doc_5Qr9tA3fZwLZmp3D1bCdEfG",
    "constancia": {
      "url": "https://api.allsign.io/v3/documents/doc_5Qr9tA3fZwLZmp3D1bCdEfG/evidence",
      "serialNumber": "12345",
      "issuedAt": "2026-07-11T19:03:30Z",
      "algorithm": "SHA256"
    },
    "evidenceSha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
  }
}
```

El SDK `@allsign/sdk` (beta —aún no publicado en npm) trae helpers para verificar la firma y tipar el sobre del evento.

## Buenas prácticas

-   **Verifica la firma** en cada entrega antes de procesar (código arriba).
-   **Deduplica por `eventId`** —puedes recibir el mismo evento más de una vez.
-   **Responde `2xx` rápido** —el timeout por intento es de 10 s; procesa en background. Un fallo transitorio se reintenta (ver [Reintentos](#reintentos-y-entregas-fallidas)).
-   **Ramifica por `livemode`** —trata los eventos `test` y `live` por caminos separados; nunca mezcles datos de prueba con producción.
-   **Usa HTTPS** —AllSign solo entrega a URLs `https://`.
-   **Rota el secreto** periódicamente con [rotate-secret](#rotate-secret); la ventana de 24 h evita downtime.
