Documentación

Webhooks de comprobantes

Cuando un comprobante de producción cambia de destino — document.authorized , document.rejected o document.failed — la plataforma lo entrega por POST a cada endpoint suscrito del libro.

El contrato de entrega#

  • El cuerpo se congela en el momento del evento: un reintento entrega la verdad de ese instante, nunca una relectura posterior.
  • X-Datil-Signature: sha256=<hex> — HMAC-SHA256 del cuerpo exacto, con el secret del endpoint como llave.
  • Tu 2xx confirma la entrega. Cualquier otra respuesta reintenta con retroceso exponencial, hasta 8 intentos; después el evento queda marcado como fallido y visible en Contabilidad → Configuración → API.
  • A lo sumo una entrega por combinación endpoint · documento · evento — pero procesa idempotente de todos modos: la red puede duplicar.

Verificación de la firma#

Calcula el HMAC sobre los bytes crudos del cuerpo — antes de cualquier parseo o re-serialización — y compara en tiempo constante. El secreto es el que definiste al crear el endpoint (se muestra una sola vez).

Entrega
POST https://erp.acme.ec/webhooks/datil
Content-Type: application/json
User-Agent: Datil-Webhooks/1.0
X-Datil-Signature: sha256=0b4e5d…

{
  "event": "document.authorized",
  "sent_at": "2026-07-03T14:07:00Z",
  "document": {
    "id": "9f2c6e1a-4b3d-4e9a-8d2f-5a7c1b9e0d43",
    "doc_type": "factura",
    "numero": "001-002-000000123",
    "access_key": "0307202601179001234500110010020000001231234567811",
    "status": "authorized",
    "issue_date": "2026-07-03",
    "currency": "USD",
    "grand_total": "115.00",
    "authorization": {
      "status": "AUTORIZADO",
      "number": "0307202601179001234500110010020000001231234567811",
      "date": "2026-07-03T14:07:00Z"
    },
    "failure_reason": null
  }
}
Elixir
# Phoenix/Plug — verifica la firma contra el cuerpo EXACTO recibido.
# Lee el cuerpo crudo ANTES de cualquier parser de JSON.
def verify(conn, secret) do
  ["sha256=" <> theirs] = Plug.Conn.get_req_header(conn, "x-datil-signature")
  {:ok, raw, conn} = Plug.Conn.read_body(conn)

  ours =
    :crypto.mac(:hmac, :sha256, secret, raw)
    |> Base.encode16(case: :lower)

  if Plug.Crypto.secure_compare(ours, theirs),
    do: {:ok, Jason.decode!(raw), conn},
    else: {:error, :bad_signature}
end
Pseudocódigo
# Pseudocódigo — cualquier lenguaje
raw      = request.raw_body            # bytes exactos, sin re-serializar
header   = request.headers["X-Datil-Signature"]   # "sha256=<hex>"
expected = hex(hmac_sha256(secret, raw))

if not constant_time_equal(expected, header.removeprefix("sha256=")):
    return 401                          # firma inválida: descarta

event = json.parse(raw)
process(event)                          # idempotente por event.document.id + event.event
return 200                              # 2xx confirma la entrega
Sin conexión — reintentando…

Esenciales

Sesión, seguridad, idioma y tema. Sin ellas el sitio no funciona.

Siempre activas