# Webhooks

> El contrato de webhooks de la API de Simplo — eventos del ciclo de vida DTE, registro de endpoints y verificación de firma Standard Webhooks.

Los webhooks empujan los cambios del ciclo de vida de un DTE a tu endpoint en
vez de que tú hagas polling.

> **Estado beta:**
> La entrega de webhooks se lanza como parte de la **beta privada** y todavía no
> está fluyendo. El registro de endpoints funciona hoy, y esta página documenta
> el contrato de entrega — eventos y esquema de firma — que las entregas usarán
> cuando se habiliten para tu cuenta. Mientras tanto, consulta el estado con
> polling de `dtes.retrieve`.

## Eventos

| Evento                 | Se dispara cuando                                        |
| ---------------------- | -------------------------------------------------------- |
| `dte.created`          | Se emitió un DTE                                         |
| `dte.accepted`         | El SII aceptó el DTE                                     |
| `dte.rejected`         | El SII rechazó el DTE                                    |
| `dte.repaired`         | El SII aceptó el DTE con observaciones (`con_reparos`)   |
| `caf.low_stock`        | Un CAF se está quedando sin folios                       |
| `certificado.expiring` | El certificado digital está próximo a vencer             |

## Endpoints de la API

| Método   | Path                            | Descripción                                     |
| -------- | ------------------------------- | ----------------------------------------------- |
| `GET`    | `/api/v1/billing/webhooks`      | Listar los webhooks registrados.                |
| `POST`   | `/api/v1/billing/webhooks`      | Registrar una URL HTTPS para recibir eventos.   |
| `DELETE` | `/api/v1/billing/webhooks/{id}` | Eliminar un webhook.                            |

## Registrar un endpoint

```ts
const webhook = await simplo.webhooks.create({
  url: 'https://tu-app.example/webhooks/simplo', // debe ser HTTPS
  events: ['dte.accepted', 'dte.rejected', 'caf.low_stock'],
});

// El secreto de firma se devuelve SOLO acá. Guárdalo ahora.
await secrets.store('SIMPLO_WEBHOOK_SECRET', webhook.secret!);
```

`simplo.webhooks.list()` muestra los endpoints registrados (secretos
enmascarados); `simplo.webhooks.delete(id)` elimina uno.

## Verifica cada entrega — siempre

Cualquiera puede hacer POST a tu endpoint. Verifica cada entrega antes de
confiar en ella. La entrega usa el esquema de firma
[Standard Webhooks](https://www.standardwebhooks.com) — este es el contrato
que las entregas llevarán cuando se habiliten, y el que implementa
`webhooks.verify`:

| Header              | Contenido                                            |
| ------------------- | ---------------------------------------------------- |
| `webhook-id`        | ID único del mensaje (estable entre reentregas)      |
| `webhook-timestamp` | Timestamp Unix (segundos)                            |
| `webhook-signature` | `v1,<HMAC-SHA256 en base64>` (lista separada por espacios) |

La firma cubre `` `${id}.${timestamp}.${rawBody}` ``. El SDK la verifica con
comparación en tiempo constante y una tolerancia de ±5 minutos en el
timestamp:

```ts
import Simplo, { WebhookVerificationError } from '@simplohq/sdk';
import express from 'express';

const simplo = new Simplo();
const app = express();

// IMPORTANTE: el body CRUDO, no el JSON parseado.
app.post('/webhooks/simplo', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const event = await simplo.webhooks.verify(
      req.body.toString('utf8'),
      req.headers,
      process.env.SIMPLO_WEBHOOK_SECRET!,
    );

    switch (event.type) {
      case 'dte.accepted':
        await marcarFacturaAceptada(event.data);
        break;
      case 'dte.rejected':
        await alertarFacturacion(event.data);
        break;
    }
    res.sendStatus(200);
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      res.sendStatus(401); // entrega no confiable — rechaza
      return;
    }
    throw error;
  }
});
```

También disponible como función standalone (sin cliente):

```ts
import { verifyWebhook } from '@simplohq/sdk';
const event = await verifyWebhook(rawBody, headers, secret, { toleranceSeconds: 300 });
```

### Reglas de verificación

1. **Verifica contra el body crudo.** Re-serializar JSON parseado cambia el
   orden de bytes/espacios y la firma no calzará.
2. **Responde 2xx rápido.** Haz el trabajo lento después de responder
   (encólalo). Las respuestas no-2xx y los timeouts se reentregan.
3. **Las entregas pueden llegar más de una vez.** Usa `webhook-id` (o tu
   propio estado por `event.data.id`) para deduplicar.
4. **El orden no está garantizado.** Trata cada evento como "algo cambió;
   re-verifica el estado" cuando el orden importe — o lee el estado
   autoritativo con `dtes.retrieve`.

## Probar localmente

Calcula una firma válida en tus tests en vez de tocar la red:

```ts
import { createHmac } from 'node:crypto';

const secret = 'test-secret';
const id = 'msg_1';
const timestamp = Math.floor(Date.now() / 1000);
const payload = JSON.stringify({ type: 'dte.accepted', data: { id: 'dte_1' } });
const signature = createHmac('sha256', secret)
  .update(`${id}.${timestamp}.${payload}`)
  .digest('base64');

const headers = {
  'webhook-id': id,
  'webhook-timestamp': String(timestamp),
  'webhook-signature': `v1,${signature}`,
};
```

## Relacionado

- [Errores](/referencia/errores/) — `WebhookVerificationError`
- [Quickstart](/comienza/quickstart/) — polling como alternativa

---

Versión HTML: https://docs.simplo.cl/referencia/webhooks/ · Índice para agentes: https://docs.simplo.cl/llms.txt
