---
title: "Recibe y verifica los webhooks de AllSign — Receta de la API v3 de AllSign"
type: how-to
canonical_url: "https://allsign.io/developers/docs/recetas/recibe-y-verifica-webhooks"
api_version: "2026-07-11"
agent_rules: [webhook-no-polling, idempotency-key, una-base-url, error-por-code, reintento-429]
last_verified: "2026-10-08T12:20:10.511Z"
---

Receta

# Recibe y verifica los webhooks de AllSign

**Objetivo:** Un endpoint que recibe los eventos de AllSign, comprueba que de verdad los firmó AllSign y sobrevive a sus propias caídas y a la rotación del secreto.

Para: backend · nivel intermedio · 15 min · Necesitas: API key de sandbox · Un endpoint HTTPS público en tu servidor

## 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.** → [Webhooks](https://allsign.io/developers/docs/webhooks)
-   **Todo `POST` que crea, cobra o envía algo lleva `Idempotency-Key` con un UUID v4 nuevo por operación.** → [Idempotencia](https://allsign.io/developers/docs/idempotency)
-   **La base URL es siempre `https://api.allsign.io/v3`; no existe una URL de sandbox aparte.** → [Entornos](https://allsign.io/developers/docs/environments)
-   **Los errores son `application/problem+json` (RFC 9457) con un `code` estable y append-only; `detail` es texto técnico en inglés que puede cambiar.** → [Errores](https://allsign.io/developers/docs/errors)
-   **Ante un `429`, espera los segundos de `Retry-After` antes de reintentar; si el `429` no trae `Retry-After`, no lo reintentes.** → [Rate limits](https://allsign.io/developers/docs/rate-limits)

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:20 (hora CDMX) · test `docs-checks-v3/tests/recetas/recibe-y-verifica-webhooks.receta.spec.ts`

Los ejemplos usan `$ALLSIGN_API_KEY`. Defínela una vez con tu key de sandbox: `export ALLSIGN_API_KEY="allsign_test_sk_…"` (en PowerShell: `$env:ALLSIGN_API_KEY="allsign_test_sk_…"`).

## 1 Consulta qué eventos puedes recibir

GET `/webhooks/events` [ver en la referencia](https://allsign.io/developers/docs/webhooks#list-events)

### Qué haces

Suscribe tu endpoint solo a los eventos que vas a procesar: cada uno es una llamada a tu servidor.

### Qué mirar

-   `data.0.event` = `"document.created"`
-   `data.0.description` = `"A document was created via the API."`

**cURL**

```
curl 'https://api.allsign.io/v3/webhooks/events' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/webhooks/events', {
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os

import requests

respuesta = requests.get(
    "https://api.allsign.io/v3/webhooks/events",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "object": "list",
  "data": [
    {
      "event": "document.created",
      "description": "A document was created via the API.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.sent",
      "description": "A document left creation and entered the signing cycle (first invitations dispatched).",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.completed",
      "description": "All parties signed and the evidence PDF is ready (delivered by stable API URL, not inline).",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.voided",
      "description": "A document was voided (annulled). NOM-151 retention keeps the record.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.expired",
      "description": "A document reached its expiry date without being completed. Carries who did manage to sign.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.owner_transferred",
      "description": "A document changed owner inside its workspace (manual transfer, bulk transfer or member offboarding). Carries the previous and the new owner.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.fill_started",
      "description": "DEPRECATED — variables are filled by the sender only, so a new document no longer waits for signers to fill data. Only the legacy fill flow still emits it, and it will stop; it stays in the catalog so existing subscriptions remain valid.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.ready_to_sign",
      "description": "The PDF was materialized with the filled data and is ready to sign.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.turn_started",
      "description": "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.",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.stage_advanced",
      "description": "Sequential signing: every signer of a stage signed and the next stage was invited.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.signed",
      "description": "One signer completed their signature. Carries the running progress.",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.fill_completed",
      "description": "DEPRECATED — signers no longer fill variables; what a signer fills goes in PDF form fields. Only the legacy fill flow still emits it, and it will stop; it stays in the catalog so existing subscriptions remain valid.",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.reminder_sent",
      "description": "A signing reminder was sent to a signer (email or WhatsApp).",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.delivery_failed",
      "description": "A signing invitation could not be delivered to a signer (WhatsApp failure or email bounce).",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "signer.declined",
      "description": "A signer declined to sign. RESERVED — the contract is frozen; it does not fire yet.",
      "category": "Signers",
      "apiVersion": "2026-07-11",
      "status": "reserved"
    },
    {
      "event": "document.observer_added",
      "description": "An observer was added to a document (or its capability/events changed). Observers never sign.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "document.observer_removed",
      "description": "An observer was removed from a document; its read-only links were revoked.",
      "category": "Documents",
      "apiVersion": "2026-07-11",
      "status": "active"
    },
    {
      "event": "nom151.constancia.issued",
      "description": "The NOM-151 conservation constancia was issued for a completed document.",
      "category": "Compliance",
      "apiVersion": "2026-07-11",
      "status": "active"
    }
  ]
}
```

## 2 Registra tu endpoint

POST `/webhooks` [ver en la referencia](https://allsign.io/developers/docs/webhooks#create-endpoint)

### Qué haces

La respuesta trae el secreto whsec\_… una sola vez: guárdalo como secreto de tu servidor, nunca en el frontend. Con él vas a comprobar que cada entrega viene de AllSign.

### Qué mirar

-   `id` = `"whe_27226709c9124d798a44032672fe2c9a"`
-   `secret` = `"whsec_…"`
-   `status` = `"enabled"`

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/webhooks' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://ejemplo.com/webhooks/allsign",
  "events": [
    "document.created",
    "document.completed"
  ],
  "description": "Avisos de firma"
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';

const respuesta = await fetch('https://api.allsign.io/v3/webhooks', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        url: 'https://ejemplo.com/webhooks/allsign',
        events: [
            'document.created',
            'document.completed',
        ],
        description: 'Avisos de firma',
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os
import uuid

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": "https://ejemplo.com/webhooks/allsign",
        "events": [
            "document.created",
            "document.completed",
        ],
        "description": "Avisos de firma",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 201**

```
{
  "livemode": false,
  "id": "whe_27226709c9124d798a44032672fe2c9a",
  "object": "webhook_endpoint",
  "url": "https://ejemplo.com/webhooks/allsign",
  "events": [
    "document.created",
    "document.completed"
  ],
  "description": "Avisos de firma",
  "status": "enabled",
  "apiVersion": "2026-07-11",
  "environment": "test",
  "secretLast4": "1NM=",
  "createdAt": "2026-07-11T18:00:00.000000Z",
  "secret": "whsec_…"
}
```

## 3 Crea un documento para que haya un evento

POST `/documents` [ver en la referencia](https://allsign.io/developers/docs/endpoints/documents#create-document)

### Qué haces

Cualquier documento sirve: al crearlo, AllSign le avisa a tu endpoint con document.created.

### Qué mirar en sandbox

-   `id` = `"doc_8a9708721fb54fcfabc32715b87f5518"`

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/documents' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Contrato para probar webhooks",
  "file": {
    "content": "'"$(base64 < contrato.pdf | tr -d '\n')"'",
    "fileType": "pdf",
    "name": "contrato.pdf"
  },
  "signers": [
    {
      "email": "signer-success@sandbox.allsign.io",
      "name": "Ana Torres"
    }
  ]
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';

const respuesta = await fetch('https://api.allsign.io/v3/documents', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        name: 'Contrato para probar webhooks',
        file: {
            content: readFileSync('contrato.pdf').toString('base64'),
            fileType: 'pdf',
            name: 'contrato.pdf',
        },
        signers: [
            {
                email: 'signer-success@sandbox.allsign.io',
                name: 'Ana Torres',
            },
        ],
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import base64
import os
import uuid
from pathlib import Path

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/documents",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Contrato para probar webhooks",
        "file": {
            "content": base64.b64encode(Path("contrato.pdf").read_bytes()).decode(),
            "fileType": "pdf",
            "name": "contrato.pdf",
        },
        "signers": [
            {
                "email": "signer-success@sandbox.allsign.io",
                "name": "Ana Torres",
            },
        ],
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 201**

```
{
  "livemode": false,
  "id": "doc_8a9708721fb54fcfabc32715b87f5518",
  "object": "document",
  "name": "Contrato para probar webhooks",
  "status": "draft",
  "documentType": "EDITABLE",
  "signerCount": 1,
  "signedCount": 0,
  "ownerId": "usr_f5fea48ae9ad4c5aafdc56145882d508",
  "orgId": "9424f74e-a378-4b13-9aff-7f05edbb5946",
  "folderId": null,
  "expiresAt": null,
  "signingOrder": "parallel",
  "currentStage": null,
  "expirationReminders": null,
  "templateId": null,
  "templateVersionId": null,
  "parentDocumentId": null,
  "createdAt": "2026-07-11T18:00:03.135000Z",
  "updatedAt": "2026-07-11T18:00:03.135000Z"
}
```

## 4 Recibe la entrega y verifica su firma

POST `tu endpoint` · evento `document.created`

### Qué haces

Antes de creerle al cuerpo, verifica la firma: webhook-signature trae v1,<HMAC-SHA256 en base64> de `webhook-id.webhook-timestamp.cuerpo crudo`, con tu secreto sin el prefijo whsec\_ y decodificado de base64. Rechaza timestamps de más de 5 minutos y usa webhook-id para descartar duplicados. Responde 2xx en cuanto la guardes y procésala después.

AllSign le hace `POST` a la URL que registraste, con el evento `document.created`. Responde `2xx` rápido y, antes de confiar en el cuerpo, verifica la firma: `webhook-signature` es un HMAC-SHA256 de `webhook-id.webhook-timestamp.cuerpo` con el secreto del webhook ([Webhooks](https://allsign.io/developers/docs/webhooks)).

Esta entrega llegó de verdad al receptor del test y su firma se verificó con el secreto del webhook.

### Qué mirar

-   `eventType` = `"document.created"`
-   `data.documentId` = `"doc_8a9708721fb54fcfabc32715b87f5518"`

### En producción

Igual con una key live. La cabecera allsign-livemode y el campo livemode te dicen si el evento es de producción o de sandbox.

**Entrega recibida · Cuerpo**

```
{
  "data": {
    "name": "Contrato para probar webhooks",
    "status": "draft",
    "signers": [
      {
        "name": "Ana Torres",
        "email": "signer-success@sandbox.allsign.io",
        "phone": null,
        "status": "pending",
        "signerId": "sgr_9d3c8ed3d60b440d9899437955abceb0"
      }
    ],
    "createdAt": "2026-07-11T18:00:04.005000Z",
    "documentId": "doc_8a9708721fb54fcfabc32715b87f5518",
    "createdViaApi": true
  },
  "eventId": "evt_0766cee1be874732a7b5b4a28bf9c025",
  "livemode": false,
  "tenantId": "058291f5-cba8-4243-a3db-cb2af4fb0058",
  "eventType": "document.created",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T18:00:04.007Z"
}
```

**Entrega recibida · Cabeceras**

```
{
  "content-type": "application/json",
  "allsign-event": "document.created",
  "allsign-livemode": "false",
  "webhook-id": "0766cee1-be87-4732-a7b5-b4a28bf9c025",
  "webhook-timestamp": "1783792804",
  "webhook-signature": "v1,<firma>",
  "allsign-delivery-id": "63de1310-00af-4375-8af9-d3de235e6f98"
}
```

**Verifica la firma · Node**

```
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verificarWebhook(cuerpoCrudo, headers, secreto) {
    const id = headers['webhook-id'];
    const timestamp = headers['webhook-timestamp'];
    if (!id || !/^\d+$/.test(timestamp ?? '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
    const llave = Buffer.from(secreto.replace(/^whsec_/, ''), 'base64');
    const esperada = Buffer.from(createHmac('sha256', llave).update(`${id}.${timestamp}.${cuerpoCrudo}`).digest('base64'));
    return (headers['webhook-signature'] ?? '').split(' ').some((token) => {
        const [version, firma = ''] = token.split(',');
        const candidata = Buffer.from(firma);
        return version === 'v1' && candidata.length === esperada.length && timingSafeEqual(candidata, esperada);
    });
}
```

**Verifica la firma · Python**

```
import base64
import hashlib
import hmac
import time


def verificar_webhook(cuerpo_crudo: bytes, headers: dict, secreto: str) -> bool:
    id_ = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp", "")
    if not id_ or not (timestamp.isascii() and timestamp.isdigit()) or abs(time.time() - int(timestamp)) > 300:
        return False
    llave = base64.b64decode(secreto.removeprefix("whsec_"))
    contenido = f"{id_}.{timestamp}.".encode() + cuerpo_crudo
    esperada = base64.b64encode(hmac.new(llave, contenido, hashlib.sha256).digest())
    return any(
        version == "v1" and hmac.compare_digest(firma.encode(), esperada)
        for version, _, firma in (token.partition(",") for token in headers.get("webhook-signature", "").split())
    )
```

## 5 Registra un endpoint que falla

POST `/webhooks` [ver en la referencia](https://allsign.io/developers/docs/webhooks#create-endpoint)

### Qué haces

Para ver qué pasa cuando tu servidor falla, este segundo endpoint de pruebas responde 500 a propósito.

### Qué mirar

-   `id` = `"whe_67554d50996d47b9b68963f6cb2fffac"`

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/webhooks' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://ejemplo.com/webhooks/allsign?status=500",
  "events": [
    "document.created"
  ],
  "description": "Endpoint caído"
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';

const respuesta = await fetch('https://api.allsign.io/v3/webhooks', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        url: 'https://ejemplo.com/webhooks/allsign?status=500',
        events: [
            'document.created',
        ],
        description: 'Endpoint caído',
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os
import uuid

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": "https://ejemplo.com/webhooks/allsign?status=500",
        "events": [
            "document.created",
        ],
        "description": "Endpoint caído",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 201**

```
{
  "livemode": false,
  "id": "whe_67554d50996d47b9b68963f6cb2fffac",
  "object": "webhook_endpoint",
  "url": "https://ejemplo.com/webhooks/allsign?status=500",
  "events": [
    "document.created"
  ],
  "description": "Endpoint caído",
  "status": "enabled",
  "apiVersion": "2026-07-11",
  "environment": "test",
  "secretLast4": "lbs=",
  "createdAt": "2026-07-11T18:00:04.771000Z",
  "secret": "whsec_…"
}
```

## 6 Revisa las entregas que fallaron

GET `/webhooks/{webhook_id}/deliveries` [ver en la referencia](https://allsign.io/developers/docs/webhooks#list-deliveries)

### Qué haces

Cada entrega guarda sus intentos en attemptHistory, con el status que respondió tu servidor. Un 5xx, un timeout o un error de red se reintentan con espera creciente durante 3 días, y mientras tanto la entrega sigue en PENDING; un 4xx la deja en FAILED de inmediato, porque AllSign entiende que tu servidor la rechazó a propósito. Tu endpoint recibe los eventos de toda tu cuenta, no solo del documento que acabas de crear, así que esta lista también puede traer entregas de otros documentos. Es lo primero que revisas cuando «no llegan los webhooks».

### Qué mirar

-   `data.0.status` = `"PENDING"`
-   `data.0.attempts` = `1`
-   `data.0.attemptHistory.0.statusCode` = `500`
-   `data.0.attemptHistory.0.outcome` = `"TRANSIENT"`

**cURL**

```
curl 'https://api.allsign.io/v3/webhooks/whe_67554d50996d47b9b68963f6cb2fffac/deliveries?limit=5' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/webhooks/whe_67554d50996d47b9b68963f6cb2fffac/deliveries?limit=5', {
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os

import requests

respuesta = requests.get(
    "https://api.allsign.io/v3/webhooks/whe_67554d50996d47b9b68963f6cb2fffac/deliveries?limit=5",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "object": "list",
  "data": [
    {
      "livemode": false,
      "id": "whd_174b06165c7f4b728a112b5886fb4aa5",
      "object": "webhook_delivery",
      "eventId": "evt_f1033b81d73e49f3bc14b9a091a7d359",
      "eventType": "document.created",
      "status": "PENDING",
      "targetUrl": "https://ejemplo.com/webhooks/allsign?status=500",
      "attempts": 1,
      "lastStatusCode": null,
      "lastError": null,
      "createdAt": "2026-07-11T18:00:06.960000Z",
      "sentAt": null,
      "attemptHistory": [
        {
          "attemptNumber": 1,
          "outcome": "TRANSIENT",
          "statusCode": 500,
          "error": "Receiver returned 500",
          "durationMs": 144,
          "attemptedAt": "2026-07-11T18:00:07.250000Z"
        }
      ]
    },
    {
      "livemode": false,
      "id": "whd_46a2ada9f93e401995a8a5cda575d84a",
      "object": "webhook_delivery",
      "eventId": "evt_c4f89267d84c43bc9b983a73eb9bdb05",
      "eventType": "document.created",
      "status": "PENDING",
      "targetUrl": "https://ejemplo.com/webhooks/allsign?status=500",
      "attempts": 1,
      "lastStatusCode": null,
      "lastError": null,
      "createdAt": "2026-07-11T18:00:06.741000Z",
      "sentAt": null,
      "attemptHistory": [
        {
          "attemptNumber": 1,
          "outcome": "TRANSIENT",
          "statusCode": 500,
          "error": "Receiver returned 500",
          "durationMs": 110,
          "attemptedAt": "2026-07-11T18:00:07.061000Z"
        }
      ]
    }
  ],
  "hasMore": false,
  "nextCursor": null,
  "previousCursor": null,
  "limit": 5
}
```

## 7 Rota el secreto

POST `/webhooks/{webhook_id}/rotate-secret` [ver en la referencia](https://allsign.io/developers/docs/webhooks#rotate-secret)

### Qué haces

Rota en cuanto sospeches que el secreto se filtró. La respuesta trae el secreto nuevo una sola vez; el anterior sigue valiendo 24 horas para que despliegues el cambio sin perder entregas.

### Qué mirar

-   `secret` = `"whsec_…"`

**cURL**

```
LLAVE=$(uuidgen)
curl -X POST 'https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $LLAVE"
```

**Node**

```
import { randomUUID } from 'node:crypto';

const llave = randomUUID();
const respuesta = await fetch('https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': llave,
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os
import uuid

import requests

llave = str(uuid.uuid4())
respuesta = requests.post(
    "https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": llave,
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "livemode": false,
  "id": "whe_27226709c9124d798a44032672fe2c9a",
  "object": "webhook_endpoint",
  "url": "https://ejemplo.com/webhooks/allsign",
  "events": [
    "document.created",
    "document.completed"
  ],
  "description": "Avisos de firma",
  "status": "enabled",
  "apiVersion": "2026-07-11",
  "environment": "test",
  "secretLast4": "lVs=",
  "createdAt": "2026-07-11T18:00:00.000000Z",
  "secret": "whsec_…"
}
```

## 8 Reintenta la rotación con la misma Idempotency-Key

POST `/webhooks/{webhook_id}/rotate-secret` [ver en la referencia](https://allsign.io/developers/docs/webhooks#rotate-secret)

Reúsa la `Idempotency-Key` del paso 7: córrelo justo después de ese paso, en la misma terminal o el mismo script.

### Qué haces

Si se cae la red y reintentas con la misma llave, no rota dos veces: recibes el mismo secreto y la cabecera Idempotency-Replayed: true.

### Qué mirar

-   `secret` = `"whsec_…"`

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $LLAVE"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': llave,
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": llave,
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "id": "whe_27226709c9124d798a44032672fe2c9a",
  "url": "https://ejemplo.com/webhooks/allsign",
  "events": [
    "document.created",
    "document.completed"
  ],
  "object": "webhook_endpoint",
  "secret": "whsec_…",
  "status": "enabled",
  "livemode": false,
  "createdAt": "2026-07-11T18:00:00.000000Z",
  "apiVersion": "2026-07-11",
  "description": "Avisos de firma",
  "environment": "test",
  "secretLast4": "lVs="
}
```

## 9 Verifica una entrega con el secreto nuevo

POST `tu endpoint` · evento `document.created`

### Qué haces

Durante esas 24 horas webhook-signature trae dos firmas separadas por espacio, una por secreto. Acepta la entrega si cualquiera cuadra con el secreto que tienes.

AllSign le hace `POST` a la URL que registraste, con el evento `document.created`. Responde `2xx` rápido y, antes de confiar en el cuerpo, verifica la firma: `webhook-signature` es un HMAC-SHA256 de `webhook-id.webhook-timestamp.cuerpo` con el secreto del webhook ([Webhooks](https://allsign.io/developers/docs/webhooks)).

Esta entrega llegó de verdad al receptor del test y su firma se verificó con el secreto del webhook.

### Qué mirar

-   `data.documentId` = `"doc_47e66108a3c64e1385ff314c388b7d6b"`

**Entrega recibida · Cuerpo**

```
{
  "data": {
    "name": "Contrato después de rotar el secreto",
    "status": "draft",
    "signers": [
      {
        "name": "Ana Torres",
        "email": "signer-success@sandbox.allsign.io",
        "phone": null,
        "status": "pending",
        "signerId": "sgr_6b457b7c97e34880908b9057b37c0f39"
      }
    ],
    "createdAt": "2026-07-11T18:00:09.212000Z",
    "documentId": "doc_47e66108a3c64e1385ff314c388b7d6b",
    "createdViaApi": true
  },
  "eventId": "evt_57fca20414f34acb829dfc3c6d46eed8",
  "livemode": false,
  "tenantId": "058291f5-cba8-4243-a3db-cb2af4fb0058",
  "eventType": "document.created",
  "apiVersion": "2026-07-11",
  "occurredAt": "2026-07-11T18:00:09.214Z"
}
```

**Entrega recibida · Cabeceras**

```
{
  "content-type": "application/json",
  "allsign-event": "document.created",
  "allsign-livemode": "false",
  "webhook-id": "57fca204-14f3-4acb-829d-fc3c6d46eed8",
  "webhook-timestamp": "1783792809",
  "webhook-signature": "v1,<firma> v1,<firma>",
  "allsign-delivery-id": "cc6efb79-3777-457a-ae95-77d2b3d8967f"
}
```

**Verifica la firma · Node**

```
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verificarWebhook(cuerpoCrudo, headers, secreto) {
    const id = headers['webhook-id'];
    const timestamp = headers['webhook-timestamp'];
    if (!id || !/^\d+$/.test(timestamp ?? '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
    const llave = Buffer.from(secreto.replace(/^whsec_/, ''), 'base64');
    const esperada = Buffer.from(createHmac('sha256', llave).update(`${id}.${timestamp}.${cuerpoCrudo}`).digest('base64'));
    return (headers['webhook-signature'] ?? '').split(' ').some((token) => {
        const [version, firma = ''] = token.split(',');
        const candidata = Buffer.from(firma);
        return version === 'v1' && candidata.length === esperada.length && timingSafeEqual(candidata, esperada);
    });
}
```

**Verifica la firma · Python**

```
import base64
import hashlib
import hmac
import time


def verificar_webhook(cuerpo_crudo: bytes, headers: dict, secreto: str) -> bool:
    id_ = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp", "")
    if not id_ or not (timestamp.isascii() and timestamp.isdigit()) or abs(time.time() - int(timestamp)) > 300:
        return False
    llave = base64.b64decode(secreto.removeprefix("whsec_"))
    contenido = f"{id_}.{timestamp}.".encode() + cuerpo_crudo
    esperada = base64.b64encode(hmac.new(llave, contenido, hashlib.sha256).digest())
    return any(
        version == "v1" and hmac.compare_digest(firma.encode(), esperada)
        for version, _, firma in (token.partition(",") for token in headers.get("webhook-signature", "").split())
    )
```

## 10 Borra el endpoint cuando ya no lo uses

DELETE `/webhooks/{webhook_id}` [ver en la referencia](https://allsign.io/developers/docs/webhooks#delete-endpoint)

### Qué haces

Borrarlo detiene las entregas de inmediato; si solo quieres pausarlo, usa PATCH con disabled: true.

**cURL**

```
curl -X DELETE 'https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a', {
    method: 'DELETE',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
    },
});
console.log(respuesta.status);
```

**Python**

```
import os

import requests

respuesta = requests.delete(
    "https://api.allsign.io/v3/webhooks/whe_27226709c9124d798a44032672fe2c9a",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
    },
)
print(respuesta.status_code)
```

**Respuesta · 204**

*Sin cuerpo.*

## Errores comunes de esta receta

Errores reales que el test de esta receta provocó a propósito en sandbox, sobre sus mismos pasos. Cada uno enlaza a su explicación en [Errores](https://allsign.io/developers/docs/errors).

`WEBHOOK_URL_INVALID` · 422 · en el paso 2 (Registra tu endpoint)

Registra un endpoint con http:// en vez de https://. [Qué significa y qué hacer](https://allsign.io/developers/docs/errors#WEBHOOK_URL_INVALID)

**Respuesta · 422 · WEBHOOK_URL_INVALID**

```
{
  "type": "https://allsign.io/developers/docs/errors#WEBHOOK_URL_INVALID",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The webhook url must be an absolute https:// URL.",
  "instance": "/v3/webhooks",
  "code": "WEBHOOK_URL_INVALID",
  "requestId": "req_b2c680669f144fcc806cb8366bae5594",
  "errors": [
    {
      "field": "url",
      "code": "INVALID_VALUE",
      "detail": "Must start with https://"
    }
  ]
}
```

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:19 (hora CDMX) · test `docs-checks-v3/tests/recetas/recibe-y-verifica-webhooks.receta.spec.ts`

`UNKNOWN_EVENT_TYPE` · 422 · en el paso 2 (Registra tu endpoint)

Suscríbete a un evento que no existe. [Qué significa y qué hacer](https://allsign.io/developers/docs/errors#UNKNOWN_EVENT_TYPE)

**Respuesta · 422 · UNKNOWN_EVENT_TYPE**

```
{
  "type": "https://allsign.io/developers/docs/errors#UNKNOWN_EVENT_TYPE",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more event types are not in the v3 catalog.",
  "instance": "/v3/webhooks",
  "code": "UNKNOWN_EVENT_TYPE",
  "requestId": "req_45571c3852cb4ac6bae34f64dc886a28",
  "errors": [
    {
      "field": "events[0]",
      "code": "UNKNOWN_EVENT_TYPE",
      "detail": "'documento.firmado' is not a valid v3 event type."
    }
  ]
}
```

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:19 (hora CDMX) · test `docs-checks-v3/tests/recetas/recibe-y-verifica-webhooks.receta.spec.ts`

`WEBHOOK_NOT_FOUND` · 404 · en `GET /v3/webhooks/whe_54c1d95de8c64a369095bc044665b5b3`

Consulta un endpoint que ya borraste. [Qué significa y qué hacer](https://allsign.io/developers/docs/errors#WEBHOOK_NOT_FOUND)

**Respuesta · 404 · WEBHOOK_NOT_FOUND**

```
{
  "type": "https://allsign.io/developers/docs/errors#WEBHOOK_NOT_FOUND",
  "title": "Not Found",
  "status": 404,
  "detail": "No webhook endpoint was found with that id.",
  "instance": "/v3/webhooks/whe_54c1d95de8c64a369095bc044665b5b3",
  "code": "WEBHOOK_NOT_FOUND",
  "requestId": "req_e424fdd005fc4298b0231312f2126597"
}
```

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:19 (hora CDMX) · test `docs-checks-v3/tests/recetas/recibe-y-verifica-webhooks.receta.spec.ts`

## Siguiente

-   [Lleva tu primer documento a firma](https://allsign.io/developers/docs/recetas/primer-documento-a-firma) — Un documento firmado de punta a punta en sandbox, con el aviso de cada firma y el de cierre llegando a tu servidor.
-   [Descarga el PDF firmado y su evidencia](https://allsign.io/developers/docs/recetas/descarga-el-pdf-firmado-y-la-evidencia) — El PDF firmado y el PDF de evidencia de un documento completado, con su huella sha256 para archivarlos.
