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

# Webhooks

> Los dieciséis eventos salientes, cómo se firman y cómo verificarlos sin equivocarse.

En vez de preguntar cada cinco minutos si cambió algo, deja que EdTools te avise. Los administradores registran endpoints HTTPS en **Ajustes → API e integraciones → Webhooks**.

<Info>
  Requiere `EDTOOLS_CLIENT_WEBHOOKS_ENABLED=1` además del módulo `platform_integrations`.
</Info>

Se entregan los cambios cubiertos por v1, **vengan de donde vengan**: del dashboard, del propio Client API o de un proceso interno. No solo los que provocaste tú.

## Los dieciséis eventos

<Tabs>
  <Tab title="Núcleo">
    * `student.created` · `student.updated`
    * `admission_application.created` · `admission_application.updated`
    * `payment.created` · `payment.updated`
    * `program.created`
    * `course.created`
  </Tab>

  <Tab title="Vault">
    * `vault.document.created` · `vault.document.uploaded` · `vault.document.updated`
    * `vault.request.created` · `vault.request.completed` · `vault.request.revoked`
  </Tab>

  <Tab title="Formularios">
    * `form_template.published`
    * `form.submitted`
  </Tab>
</Tabs>

<Warning>
  `program.updated` y `course.updated` **no existen todavía**. Si tu integración necesita enterarse de cambios en el catálogo, por ahora tiene que consultar con `updatedSince`.
</Warning>

## Cada entrega

```http theme={null}
POST /tu-endpoint
X-EdTools-Event-Id: <uuid>
X-EdTools-Event-Type: student.created
X-EdTools-Delivery-Id: <uuid>
X-EdTools-Timestamp: 2026-05-09T00:00:00.000Z
X-EdTools-Signature: v1=<hmac_sha256>
```

`Event-Id` identifica el hecho; `Delivery-Id` identifica el intento. Si reintentamos, el `Event-Id` se repite y el `Delivery-Id` no — **desduplica por `Event-Id`**.

## La firma

```text theme={null}
firma = "v1=" + HMAC_SHA256(secreto, timestamp + "." + cuerpo_crudo)
```

Tres detalles que hay que respetar, o la verificación falla sin motivo aparente:

<AccordionGroup>
  <Accordion title="Firma el cuerpo crudo, no el JSON reserializado">
    Si tu framework parsea el body antes de que lo veas, guarda el texto original. Un `JSON.parse` seguido de `JSON.stringify` cambia los bytes y rompe la firma.
  </Accordion>

  <Accordion title="Compara en tiempo constante">
    Un `===` sobre la firma filtra información por el tiempo de comparación. Usa `timingSafeEqual` o el SDK.
  </Accordion>

  <Accordion title="Rechaza timestamps viejos">
    La tolerancia por defecto es de **5 minutos** en cualquiera de las dos direcciones. Sin esa ventana, una entrega capturada sirve para siempre.
  </Accordion>
</AccordionGroup>

## Verificar con el SDK

```ts theme={null}
import { verifyEdToolsWebhook } from '@edtools/client';

const resultado = verifyEdToolsWebhook({
  secret: process.env.EDTOOLS_WEBHOOK_SECRET!,
  rawBody,
  headers: request.headers,
});

if (!resultado.ok) {
  // 'missing_signature' | 'missing_timestamp' | 'invalid_timestamp'
  // | 'timestamp_out_of_tolerance' | 'invalid_signature'
  throw new Error(resultado.reason);
}
```

Acepta un objeto `Headers` o un diccionario plano, y `toleranceSeconds` si necesitas otra ventana. Para probar tu receptor en local, `signEdToolsWebhook` genera firmas válidas con el mismo algoritmo.

## Reintentos

Éxito es cualquier `2xx` **antes de 10 segundos**. Los `3xx`, `4xx`, `5xx` y los timeouts quedan registrados y se reintentan desde Trigger.dev; un administrador también puede reintentar a mano desde **Entregas**.

<Tip>
  Responde `200` en cuanto valides la firma y encola el trabajo. Si procesas de forma síncrona y tardas once segundos, EdTools lo cuenta como fallo y te manda el evento otra vez — con lo que acabas haciendo el trabajo dos veces.
</Tip>
