Webhooks (e-CF recibidos)

Webhooks — e-CF recibidos

Cuando un proveedor emite una factura electrónica (e-CF) a nombre del RNC de tu empresa, la recibimos automáticamente. Si configuras un webhook, te reenviamos ese e-CF a tu propio endpoint en cuanto llega — así puedes integrarlo con tu ERP/contabilidad sin hacer polling.

  • Asíncrono: el reenvío ocurre en segundo plano; no bloquea la recepción.
  • Con reintentos: si tu endpoint falla, reintentamos automáticamente.
  • Firmado: cada request lleva una firma HMAC-SHA256 para que verifiques que proviene de nosotros.

Configuración

Desde el portal → Webhook:

  1. Activa el webhook y coloca la URL de tu endpoint (https://…).
  2. (Opcional) Agrega headers personalizados que enviaremos en cada llamada — por ejemplo tu propia API key: Authorization: Bearer ….
  3. Copia el secreto de firma (lo necesitas para verificar la firma).

El reenvío se activa solo para e-CF recibidos (facturas de tus proveedores). No aplica a los comprobantes que emites vía el API.

El request que enviamos

POST a tu URL con Content-Type: application/json:

POST https://tu-servidor.com/webhooks/ecf
Content-Type: application/json
X-Appwork-Signature: sha256=<hmac-hex>
X-Appwork-Event: received_ecf
X-Appwork-Delivery: <id-de-entrega>
Authorization: Bearer <tu-header-personalizado>   # si lo configuraste

Cuerpo (JSON)

{
  "event": "received_ecf",
  "sentAt": "2026-07-23T15:04:05.000Z",
  "receivedEcf": {
    "id": "b3f1c2d4-…",
    "encf": "E310000000123",
    "tipoEcf": "E31",
    "rncEmisor": "101672919",
    "rncComprador": "101672919",
    "montoTotal": 11800.0,
    "fechaEmision": "2026-07-20",
    "approvalStatus": "pending",
    "receivedAt": "2026-07-23T15:04:04.000Z"
  },
  "xml": "<?xml version=\"1.0\"?><ECF>…</ECF>"
}

El campo xml contiene el comprobante completo (firmado). Los campos de receivedEcf son un resumen para que no tengas que parsear el XML si solo necesitas los montos y el estado.

Verificar la firma

Calcula el HMAC-SHA256 del cuerpo crudo (bytes exactos que recibiste) con tu secreto, en hexadecimal, y compáralo (en tiempo constante) con el valor del header X-Appwork-Signature (sin el prefijo sha256=).

import crypto from 'crypto'
 
function verify(rawBody, signatureHeader, secret) {
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')
  const a = Buffer.from(signatureHeader)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
 
// Express (usa el cuerpo crudo, NO el ya parseado por JSON):
app.post('/webhooks/ecf', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('X-Appwork-Signature')
  if (!verify(req.body, sig, process.env.APPWORK_WEBHOOK_SECRET)) {
    return res.status(401).send('bad signature')
  }
  const payload = JSON.parse(req.body.toString('utf8'))
  // … procesa payload.receivedEcf / payload.xml
  res.sendStatus(200)   // responde 2xx para confirmar la entrega
})
🔐

Verifica siempre la firma sobre el cuerpo crudo. Si tu framework ya parseó el JSON, re-serializar puede cambiar bytes (orden/espacios) y la firma no coincidirá. Usa el raw body.

Respuesta esperada y reintentos

  • Responde con un código 2xx (o 3xx) para confirmar que recibiste el e-CF.
  • Cualquier otro código, timeout o error de red se considera fallo y lo reintentamos automáticamente con backoff, hasta agotar los intentos.
  • Puedes ver el estado de cada entrega e reintentar manualmente desde el portal → Webhook → Historial de entregas.

Tu endpoint debe ser idempotente: ante un reintento podrías recibir el mismo e-CF más de una vez. Usa el encf (o X-Appwork-Delivery) para deduplicar.

Buenas prácticas

  • Responde rápido (≤ 20s). Si tu procesamiento es pesado, encola y responde 2xx de inmediato.
  • Rota el secreto desde el portal si sospechas que se filtró.
  • Restringe por IP/headers si tu endpoint es sensible.