> ## Documentation Index
> Fetch the complete documentation index at: https://formpaste.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recibe un HTTP POST firmado por cada envío entregado - Pro.

<Info>**Pro** - configurar y recibir entregas de webhook requiere Pro.</Info>

Formpaste puede enviar un `POST` de cada envío que no sea spam a una URL
que controlas, en tiempo real, firmado para que puedas verificar que
realmente vino de Formpaste.

## Activar un webhook

1. Abre un formulario en el [panel](https://app.formpaste.com) → **Settings** → **Webhooks**.
2. Ingresa la URL `https://` de tu endpoint (`webhook_url`) y guarda.
3. El panel muestra un **secreto de firma** (`whsec_…`) para este
   formulario, cópialo; lo necesitarás para verificar las solicitudes
   entrantes.

## Cuándo se activa

Un webhook se activa por cada envío **entregado, que no sea spam**, los
mismos envíos que generan un correo de notificación. Los envíos en
cuarentena (spam) no activan un webhook. La entrega es de mejor esfuerzo e
independiente del correo: una falla de webhook nunca bloquea ni afecta tu
notificación por correo.

## Solicitud

`POST` a tu URL configurada, `Content-Type: application/json`.

| Header                  | Value                                                          |
| ----------------------- | -------------------------------------------------------------- |
| `X-Formpaste-Event`     | `submission.received`                                          |
| `X-Formpaste-Timestamp` | Época Unix en **milisegundos** de cuando se firmó la solicitud |
| `X-Formpaste-Signature` | `sha256=<hex>` - HMAC-SHA256 (consulta Verificar abajo)        |
| `User-Agent`            | `Formpaste-Webhook/1`                                          |

## Payload

El cuerpo es un envoltorio JSON versionado. Tus campos enviados viven bajo
`data`:

```json theme={null}
{
  "type": "submission.received",
  "version": 1,
  "id": "sub_...",
  "form_id": "frm_...",
  "created_at": 1720700000000,
  "data": {
    "name": "John Smith",
    "email": "john@example.com",
    "message": "Hello!"
  }
}
```

* `type` / `version`: el envoltorio. Hoy solo existe `submission.received`
  (versión `1`); nuevos tipos de evento pueden agregarse más adelante.
* `data`: tus campos enviados, ya limpios: `access_key`, el honeypot
  `botcheck`, y cualquier campo de control `redirect` se eliminan y nunca
  se incluyen.

<Note>
  Los campos están anidados bajo `data`, no aplanados en el nivel
  superior, en Zapier, Make, Pipedream, o n8n, mapea `data.email`,
  `data.name`, etc.
</Note>

## Verificar la firma

La firma es `HMAC-SHA256` sobre la cadena `${timestamp}.${rawBody}`, con
clave el secreto de firma de tu formulario (`whsec_…`, mostrado en el
panel; puedes regenerarlo en cualquier momento). Compárala con
`X-Formpaste-Signature` (`sha256=<hex>`), y rechaza solicitudes cuyo
`X-Formpaste-Timestamp` sea demasiado antiguo para tu tolerancia.

```js theme={null}
import crypto from "node:crypto";

function verify(rawBody, headers, secret, toleranceMs = 5 * 60_000) {
  const ts = Number(headers["x-formpaste-timestamp"]);
  if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > toleranceMs) return false;

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const got = headers["x-formpaste-signature"] ?? "";

  const a = Buffer.from(expected);
  const b = Buffer.from(got);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Firma sobre los **bytes crudos del cuerpo de la solicitud**, antes de
cualquier re-serialización JSON, una recodificación puede reordenar claves
y romper la comparación.

## Reintentos, backoff y registro de entregas

Si tu endpoint no devuelve un `2xx`, Formpaste reintenta con backoff
exponencial, aproximadamente **10 s → 60 s → 5 min → 30 min**, hasta **5
intentos en total** durante unos 35 minutos, luego la entrega se marca
como terminal (dead-letter). Una respuesta `4xx` se trata como un fallo
permanente y no se reintenta, corrige tu endpoint y luego reenvíala
manualmente. Los errores de red y los timeouts (10 s) se reintentan igual
que un `5xx`/`429`.

Tu endpoint debería responder `2xx` rápidamente y hacer el trabajo lento
de forma asíncrona.

El panel muestra un **registro de entregas** por formulario (evento,
estado, código HTTP, marca de tiempo) con una acción de **Replay** para
reenviar una entrega pasada una vez que tu endpoint esté arreglado.

## Requisitos y límites

* La URL debe ser **`https://`**, el HTTP simple se rechaza. Las
  direcciones loopback, de red privada, link-local, y de metadatos en la
  nube se rechazan como protección SSRF, tanto al guardar como de nuevo
  antes de cada envío.
* Una URL de webhook por formulario.

## Enviar a Slack / Discord

Un webhook crudo de Formpaste no puede publicar directamente en una URL de
webhook de Slack o Discord, esas esperan su propia forma de cuerpo (Slack:
`{"text": …}`, Discord: `{"content": …}`) y rechazarán el envoltorio de
arriba. Apunta tu webhook de Formpaste a una plataforma de automatización
(Zapier, Make, Pipedream, n8n) que reformatee el payload al formato del
servicio de chat, y luego haz que esa automatización publique en
Slack/Discord. Todavía no hay una integración de Slack/Discord integrada y
lista para usar, consulta las guías de [Slack](/docs/es/guides/slack-notifications)
y [Discord](/docs/es/guides/discord-notifications) para la receta de relevo de
webhook.

<Note>
  Google Sheets es una integración nativa y real: conecta una
  hoja y las filas llegan automáticamente, sin necesidad de webhook. Los disparadores
  "nativos" de Zapier, Notion y las integraciones directas de Airtable aún no están
  construidos; para esos, el webhook genérico de arriba es la ruta funcional de hoy.
</Note>

## Free vs. Pro

Los formularios Free no pueden configurar un webhook. Intentar guardar un
`webhook_url` en Free devuelve:

| Código             | HTTP | Significado                        |
| ------------------ | ---- | ---------------------------------- |
| `upgrade_required` | 403  | Esta función requiere un plan Pro. |

## Sin configuración

Ningún webhook se activa hasta que estableces explícitamente un
`webhook_url` en un formulario del plan Pro, un formulario nuevo no envía
nada.
