Saltar al contenido
DevFact

Documentación

Webhooks

Avisos firmados cuando un comprobante cambia de estado, con la verificación de firma.

Crear un webhook

En Webhooks agrega la URL de tu servidor y elige los eventos, o créalo por la API. El secreto whsec_… se muestra una sola vez: guárdalo para verificar la firma.

POST /v1/webhooks
curl -X POST "https://api.devfact.fonles.com/v1/webhooks" \
  -H "Authorization: Bearer $DEVFACT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://tu-erp.ejemplo.com/webhooks/devfact",
  "eventos": [
    "comprobante.autorizado",
    "comprobante.no_autorizado",
    "comprobante.devuelto"
  ]
}'

Eventos

  • comprobante.autorizadoEl SRI autorizó el comprobante (también los que respondieron 202).
  • comprobante.no_autorizadoEl SRI no lo autorizó (error de autorización).
  • comprobante.devueltoLa recepción del SRI lo devolvió.
  • comprobante.anuladoSe marcó como anulado.
  • comprobante.atascadoLleva varias horas sin estado final (por ejemplo, con el SRI caído).
  • comprobante.correo_fallidoEl correo al comprador rebotó o no se pudo enviar.
  • certificado.por_vencerTu firma vence en 30, 15, 7 o 1 día.
  • certificado.vencidoTu firma venció: no puedes emitir en producción hasta subir otra.
  • aprobacion.requeridaUn agente pidió emitir algo que necesita tu visto bueno.

Sin eventos (o con "*") recibes todos, también los que se agreguen después. Los de certificado no son de un comprobante: su data trae certificadoId, ruc y diasRestantes.

POST a tu URLJSON
{
  "type": "comprobante.autorizado",
  "timestamp": "2026-11-04T15:15:09.612Z",
  "apiVersion": "2026-11-01",
  "data": {
    "id": "e9adcca8-8817-4212-911f-9a9f511f8a02",
    "estado": "AUTORIZADO",
    "tipo": "factura",
    "numero": "001-001-000004521",
    "claveAcceso": "0411202601179999999000110010010000045211234567813",
    "importeTotal": "11.50",
    "correoEstado": "ENVIADO",
    "ultimoError": null,
    "simulado": false
  }
}
El payload no lleva datos del comprador: sólo ids, estado, número, clave de acceso e importe.

Verificar la firma

Cada entrega trae las cabeceras webhook-id, webhook-timestamp y webhook-signature (estándar Standard Webhooks). La firma es v1,base64(HMAC-SHA256(secreto, "id.timestamp.cuerpo")), con el secreto sin el prefijo whsec_ decodificado de base64. Verifica sobre el cuerpo crudo, antes de parsear el JSON.

Verificar la firmaJavaScript
import crypto from "node:crypto";
import express from "express";

const SECRETO = process.env.DEVFACT_WEBHOOK_SECRET; // whsec_…

function firmaValida(cuerpo, cabeceras) {
  const id = cabeceras["webhook-id"];
  const ts = cabeceras["webhook-timestamp"];
  const firmas = cabeceras["webhook-signature"] ?? "";
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const clave = Buffer.from(SECRETO.replace(/^whsec_/, ""), "base64");
  const esperada = crypto.createHmac("sha256", clave).update(`${id}.${ts}.${cuerpo}`).digest();
  return firmas.split(" ").some((f) => {
    const [version, firma] = f.split(",");
    const recibida = Buffer.from(firma ?? "", "base64");
    return version === "v1" && recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada);
  });
}

const app = express();
// El cuerpo crudo: la firma se calcula sobre los bytes exactos que llegaron.
app.post("/webhooks/devfact", express.raw({ type: "application/json" }), (req, res) => {
  const cuerpo = req.body.toString("utf8");
  if (!firmaValida(cuerpo, req.headers)) return res.status(401).end();

  const evento = JSON.parse(cuerpo);
  // Deduplica por req.headers["webhook-id"] y encola el trabajo.
  if (evento.type === "comprobante.autorizado") {
    console.log("Autorizado:", evento.data.numero, evento.data.claveAcceso);
  }
  res.status(204).end();
});

app.listen(3000);

Buenas prácticas

  • Responde 2xx en menos de 10 segundos y procesa después (una cola). Si no, se reintenta hasta 12 veces con espera creciente.
  • La entrega es al menos una vez: deduplica por webhook-id, que no cambia entre reintentos.
  • Rechaza entregas con un webhook-timestamp de hace más de 5 minutos.
  • Los eventos pueden llegar desordenados: si te importa el estado actual, consulta el comprobante.