Skip to content
SimploSimplo Docs

Webhooks

The Simplo API's webhook contract — DTE lifecycle events, endpoint registration and Standard Webhooks signature verification. Delivery isn't live yet — use polling.

Webhooks push DTE lifecycle changes to your endpoint instead of you polling for them.

Event Fires when
dte.created A DTE was emitted
dte.accepted The SII accepted the DTE
dte.rejected The SII rejected the DTE
dte.repaired The SII accepted the DTE with observations (con_reparos)
caf.low_stock A CAF is running low on folios
certificado.expiring The digital certificate is close to expiring
Method Path Description
GET /api/v1/billing/webhooks List registered webhooks.
POST /api/v1/billing/webhooks Register an HTTPS URL to receive events.
DELETE /api/v1/billing/webhooks/{id} Delete a webhook.

These endpoints are session-only (your account’s JWT): a billing:dte API key can’t call them (403 BILLING_API_KEY_ROUTE_NOT_ALLOWED) — webhooks are managed from your account, not from the integration.

const webhook = await simplo.webhooks.create({
url: 'https://tu-app.example/webhooks/simplo', // must be HTTPS
events: ['dte.accepted', 'dte.rejected', 'caf.low_stock'],
});
// The signing secret is returned ONLY here. Save it now.
await secrets.store('SIMPLO_WEBHOOK_SECRET', webhook.secret!);

simplo.webhooks.list() shows registered endpoints (secrets masked); simplo.webhooks.delete(id) removes one.

Anyone can POST to your endpoint. Verify every delivery before trusting it. Delivery uses the Standard Webhooks signature scheme — this is the contract deliveries will carry once enabled, and the one webhooks.verify implements:

Header Content
webhook-id Unique message ID (stable across redeliveries)
webhook-timestamp Unix timestamp (seconds)
webhook-signature v1,<HMAC-SHA256 in base64> (space-separated list)

The signature covers `${id}.${timestamp}.${rawBody}`. The SDK verifies it with a constant-time comparison and a ±5 minute timestamp tolerance:

import Simplo, { WebhookVerificationError } from '@simplohq/sdk';
import express from 'express';
const simplo = new Simplo();
const app = express();
// IMPORTANT: the RAW body, not the parsed JSON.
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); // untrusted delivery — reject it
return;
}
throw error;
}
});

Also available as a standalone function (no client needed):

import { verifyWebhook } from '@simplohq/sdk';
const event = await verifyWebhook(rawBody, headers, secret, { toleranceSeconds: 300 });
  1. Verify against the raw body. Re-serializing parsed JSON changes byte/whitespace order and the signature won’t match.
  2. Respond 2xx fast. Do slow work after responding (queue it). Non-2xx responses and timeouts get redelivered.
  3. Deliveries can arrive more than once. Use webhook-id (or your own state keyed on event.data.id) to deduplicate.
  4. Order isn’t guaranteed. Treat each event as “something changed; re-verify the state” when order matters — or read the authoritative state with dtes.retrieve.

Compute a valid signature in your tests instead of hitting the network:

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}`,
};