Comges DTE

Buscar en la documentación

Escribí para filtrar guías, endpoints y webhooks.

Guía

Autoservicio en el portal

Webhooks

En vez de pollear GET /dte/{id} hasta que el estado se estabilice, recibís un POST firmado en tu servidor cada vez que pasa algo: un documento cambia de estado ante el SII, llega un DTE de un proveedor, o se te están acabando los folios.

Catálogo de eventos

Son 17 tipos y el catálogo es cerrado: un tipo que no esté acá no se publica. Al suscribirte elegís una lista de tipos, o * para recibir todos, incluidos los que agreguemos más adelante. Los nombres siguen la forma dominio.hecho y van en pasado: el webhook informa algo que ya ocurrió. Agregar un tipo nuevo es retrocompatible; renombrar uno no lo es, y por eso no lo hacemos sin versionar.

Emisión

El documento propio, antes de que el SII opine. Sirven para numerar la venta sin esperar el ciclo tributario completo.

tipoCuándo se disparaQué trae en datos
dte.emitidoEl documento existe: folio asignado, XML firmado y persistido. Se publica antes de ir al SII.documentoId, tipoDte, folio, montoTotal, fechaEmision, rutReceptor y razonSocialReceptor. estadoSii viene 0 (Pendiente) y trackId null: todavía no hay sobre. En una NC/ND llega además la referencia (codRef + documentoReferenciado).
dte.anuladoSe anuló un documento con una nota de crédito 61 (codRef 1) y el original quedó marcado como anulado.El documento ORIGINAL, con estadoSii 5 (Anulado). codRef y documentoReferenciado llegan en null —ver el aviso de abajo— y el folio de la nota de crédito va en detalle, legible.

Ciclo SII

El recorrido del documento ante el SII. Son los que reemplazan al polling: en vez de repetir la consulta, esperás el aviso.

tipoCuándo se disparaQué trae en datos
dte.enviadoEl SII recibió el sobre y devolvió trackId. A partir de acá conviene esperar el estado final.trackId ya con valor y estadoSii 1 (Enviado).
dte.aceptadoEl SII aceptó el documento. Estado terminal.estadoSii 2 y detalle con lo que respondió el SII.
dte.aceptado_con_reparosEl SII lo aceptó pero observó algo. Es válido tributariamente: el documento existe y tiene folio.estadoSii 4 y detalle con el reparo. Vale la pena revisarlo a mano.
dte.rechazadoEl SII rechazó el documento. Estado terminal.estadoSii 3 y detalle con el motivo del rechazo.
dte.error_envioEl sobre no se pudo subir (sin token, error HTTP, timeout). No es terminal: el documento sigue emitido y el envío se reintenta solo.estadoSii 6, trackId null y detalle con el error del intento.

Recepción de proveedores

Documentos que otros te emiten a vos, y los acuses que respondés. Acá rutEmisor es el proveedor y rutReceptor sos vos: es el único grupo donde los roles se invierten.

tipoCuándo se disparaQué trae en datos
recepcion.documento_recibidoLlegó un DTE de un proveedor y quedó persistido, después del dedupe.tipoDte, folio, montoTotal y fechaEmision del documento del proveedor. rutEmisor es el proveedor; rutReceptor y razonSocialReceptor son tu empresa.
recepcion.documento_aceptadoSe envió el acuse comercial de aceptación del documento del proveedor.El documento del proveedor que se aceptó.
recepcion.documento_rechazadoSe envió el acuse comercial de rechazo.El documento del proveedor, con el motivo en detalle.
recepcion.documento_reclamadoSe reclamó el documento dentro de los 8 días hábiles. Todavía no se publica: ver «Eventos declarados que aún no se publican».
recepcion.acuse_tacitoVenció el plazo legal sin acuse y quedó registrada la aceptación tácita. Lo dispara un proceso programado, no una acción tuya.El documento del proveedor que quedó aceptado por vencimiento.
recepcion.recibo_mercaderiaSe registró el recibo de mercaderías de la Ley 19.983.El documento del proveedor sobre el que se firmó el recibo.

Operativos

Avisos de infraestructura tributaria: lo que se te va a acabar antes de que te deje sin emitir. No hablan de un documento, así que casi todo el bloque útil viaja en extra.

tipoCuándo se disparaQué trae en datos
caf.folios_bajosQuedan pocos folios disponibles para un tipo de DTE. El umbral es configurable, y un barrido diario lo evalúa.tipoDte del CAF y extra con foliosDisponibles, tipoDte y umbral. folio, trackId y estadoSii en null.
caf.por_vencerUn CAF vence pronto: hay que reobtener folios antes de quedarse sin timbrar. El aviso es por CAF, no por tipo.tipoDte y extra con folioDesde, folioHasta, fechaVencimiento (AAAA-MM-DD), diasRestantes y cafId.
certificado.por_vencerEl certificado digital de la empresa vence pronto. Todavía no se publica: ver «Eventos declarados que aún no se publican».

Prueba

Un solo tipo, sintético, para verificar el destino sin esperar a que ocurra algo real.

tipoCuándo se disparaQué trae en datos
webhook.pruebaLo dispara el simulador del portal contra una suscripción puntual. No se suscribe: se dispara.Datos de relleno con la forma completa del payload, y extra con {"prueba": true}. Su clave de idempotencia siempre es única, así que cada prueba llega como un evento nuevo y nunca se deduplica contra la anterior.

Eventos declarados que aún no se publican

Acá sólo se describe lo que existe. Dos tipos del catálogo se pueden elegir en el selector, pero hoy no los publica nadie. Si te suscribís a ellos no vas a recibir nada, y no hay ningún error que te lo avise.

tipoEstado real
certificado.por_vencerSin productor. El tipo existe en el catálogo y se puede suscribir, pero todavía no lo publica nadie. Mientras tanto, la fecha de vencimiento del certificado se mira en el portal.
recepcion.documento_reclamadoSin punto de publicación. El tipo está declarado, pero el reclamo al SII no pasa hoy por un camino que publique el evento.

Los dos van a empezar a llegar sin aviso previo el día que se implementen —que un tipo empiece a publicarse es retrocompatible—, así que si ya los tenés suscritos, tu receptor debería ignorarlos sin romperse en vez de asumir que nunca llegan.

El cuerpo que recibís

Se manda con Content-Type: application/json. Es exactamente el cuerpo que se firma. La forma es la misma para todos los tipos: lo que cambia es qué campos vienen con valor y qué campos vienen null.

dte.aceptado — el caso típico
{
  "id": "0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11",
  "tipo": "dte.aceptado",
  "version": 1,
  "ocurridoEn": "2026-08-26T12:30:00Z",
  "ambiente": "Produccion",
  "empresaRut": "76665850-4",
  "datos": {
    "documentoId": "8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88",
    "tipoDte": 33,
    "folio": 1234,
    "trackId": 987654321,
    "estadoSii": 2,
    "estadoSiiNombre": "Aceptado",
    "detalle": "Aceptado por el SII",
    "codRef": null,
    "codRefNombre": null,
    "documentoReferenciado": null,

    "rutEmisor": "76665850-4",
    "rutReceptor": "96790240-3",
    "razonSocialReceptor": "Cliente Ejemplo S.A.",
    "montoTotal": 119000,
    "fechaEmision": "2026-08-26",

    "extra": {}
  }
}
CampoQué es
idIdentificador del evento. Estable entre reintentos: es la clave por la que tenés que deduplicar.
tipoUno de los tipos del catálogo. Viaja también en X-Comges-Event-Type.
versionVersión del formato del cuerpo. Hoy siempre 1.
ocurridoEnMomento real del hecho, en UTC (ISO 8601). No es el momento del intento de entrega.
ambienteCertificacion o Produccion. Una suscripción vive en un solo ambiente.
empresaRutRUT de tu empresa, la dueña de la suscripción. En los eventos de recepción sigue siendo el tuyo, no el del proveedor.
datos.documentoIdEn emisión y ciclo SII, el mismo id que devolvió la emisión y que usás en GET /dte/{id}. En los eventos recepcion.* es el id del documento recibido, que esa ruta no conoce; en los caf.* es un ancla de deduplicación, no un documento. null si el evento no tiene sujeto.
datos.tipoDteCódigo SII del tipo, como número (33, 34, 39, 41, 52, 56, 61, 110, 111, 112).
datos.folioFolio del documento.
datos.trackIdIdentificador del sobre en el SII. null mientras no haya sobre subido.
datos.estadoSiiCódigo numérico del estado (tabla más abajo).
datos.estadoSiiNombreEl mismo estado en texto.
datos.detalleTexto legible: lo que respondió el SII, el motivo del error, o la descripción del aviso operativo. En dte.anulado trae el folio de la nota de crédito que anuló el documento. Puede venir null.
datos.codRefSolo cuando el DTE del evento referencia a otro (NC y ND): 1 anula el documento completo, 2 corrige texto, 3 corrige monto (devolución parcial). null en el resto.
datos.codRefNombreEl mismo codRef en texto: Anula documento, Corrige texto, Corrige monto.
datos.documentoReferenciadoEl documento al que apunta el DTE del evento: { "tipoDte": "33", "folio": "1234" }. null cuando no referencia a nadie. Ojo con los tipos — ver el aviso de abajo.
datos.rutEmisorQuién emitió el documento. En emisión y ciclo SII sos vos; en los eventos recepcion.* es el proveedor.
datos.rutReceptorQuién lo recibe. En emisión es tu cliente; en los eventos recepcion.* sos vos.
datos.razonSocialReceptorRazón social del receptor, para no tener que ir a buscarla.
datos.montoTotalMonto total del documento, en la moneda del documento, como número.
datos.fechaEmisionFecha de emisión, AAAA-MM-DD. Es una fecha de negocio, sin hora ni zona: no la parsees como instante o vas a mostrar el día anterior.
datos.extraDiccionario abierto con lo propio de cada evento que no habla de un documento (caf.*, certificado.*, webhook.prueba). Llega {} cuando no hay nada que agregar. Leelo defensivamente: puede crecer sin previo aviso.

Códigos de estadoSii

CódigoNombre
0Pendiente
1Enviado
2Aceptado
3Rechazado
4AceptadoConReparos
5Anulado
6ErrorEnvio

Recepción: los RUT se invierten

Es el punto contraintuitivo de todo el contrato, y conviene leerlo dos veces. En los eventos recepcion.* el documento no es tuyo: te lo emitió un proveedor. Entonces datos.rutEmisor es el proveedor y datos.rutReceptor sos vos —igual que razonSocialReceptor, que trae tu razón social, no la del proveedor—.

empresaRut en cambio nunca cambia de dueño: siempre es el RUT de la empresa que tiene la suscripción. Es el campo por el que ruteás si atendés varias empresas con un mismo endpoint. Si escribiste el receptor pensando sólo en emisión y asumís que rutEmisor === empresaRut, el primer recepcion.documento_recibido te va a romper esa suposición en silencio.

recepcion.documento_recibido — el emisor es el proveedor
{
  "id": "c81d4a90-5f27-4b6e-9d33-0a71e5c8b402",
  "tipo": "recepcion.documento_recibido",
  "version": 1,
  "ocurridoEn": "2026-08-26T09:14:05Z",
  "ambiente": "Produccion",
  "empresaRut": "76665850-4",
  "datos": {
    "documentoId": "1d9b7f44-2c60-4a8e-b0f1-6e3d5a90c7b2",
    "tipoDte": 33,
    "folio": 55120,
    "trackId": null,
    "estadoSii": null,
    "estadoSiiNombre": null,
    "detalle": "Documento recibido de un proveedor",
    "codRef": null,
    "codRefNombre": null,
    "documentoReferenciado": null,

    "rutEmisor": "96790240-3",
    "rutReceptor": "76665850-4",
    "razonSocialReceptor": "Mi Empresa SpA",
    "montoTotal": 476000,
    "fechaEmision": "2026-08-25",

    "extra": {}
  }
}

Eventos operativos: todo lo propio va en extra

caf.* y certificado.* no hablan de un documento: no hay folio, ni estado, ni montos —y el documentoId que ves no apunta a ningún documento, es un ancla de deduplicación—. La información específica viaja en datos.extra, un diccionario abierto. En los eventos de documento llega vacío ({}), nunca ausente.

caf.folios_bajos — el contenido útil está en extra
{
  "id": "2b6f0c81-9e34-4d17-8a52-c470b1f9e335",
  "tipo": "caf.folios_bajos",
  "version": 1,
  "ocurridoEn": "2026-08-26T12:20:00Z",
  "ambiente": "Produccion",
  "empresaRut": "76665850-4",
  "datos": {
    "documentoId": "6f1c0a55-2d38-4b71-9e04-8a5c3d2f7b19",
    "tipoDte": 33,
    "folio": null,
    "trackId": null,
    "estadoSii": null,
    "estadoSiiNombre": null,
    "detalle": "Quedan 12 folio(s) disponibles para el tipo 33 (umbral de aviso: 50).",
    "codRef": null,
    "codRefNombre": null,
    "documentoReferenciado": null,

    "rutEmisor": null,
    "rutReceptor": null,
    "razonSocialReceptor": null,
    "montoTotal": null,
    "fechaEmision": null,

    "extra": {
      "foliosDisponibles": 12,
      "tipoDte": 33,
      "umbral": 50
    }
  }
}

Las claves de extra dependen del tipo: caf.folios_bajos manda foliosDisponibles, tipoDte y umbral; caf.por_vencer manda folioDesde, folioHasta, fechaVencimiento, diasRestantes, tipoDte y cafId. Leelo con ?. o su equivalente: es el único bloque del payload que puede sumar claves nuevas sin que cambie nada más.

Headers

Request tal como llega a tu servidor
POST /webhooks/comges HTTP/1.1
Content-Type: application/json
X-Comges-Signature: t=1774704312,v1=66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd
X-Comges-Timestamp: 1774704312
X-Comges-Event-Id: 0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11
X-Comges-Event-Type: dte.aceptado
X-Comges-Delivery-Id: 5b2e7d10-9c44-4f0a-8e31-77a6b0c9d123
X-Comges-Delivery-Attempt: 1
HeaderContenido
X-Comges-Signaturet=<unix>,v1=<hex minúscula>
X-Comges-TimestampSegundos Unix UTC. Es el mismo valor que el t= de la firma, y entra en el material firmado.
X-Comges-Event-IdId del evento. Estable entre reintentos — deduplicá por acá.
X-Comges-Event-TypeEl tipo del evento, para rutear sin parsear el cuerpo.
X-Comges-Delivery-IdId de la entrega. Cambia por suscripción, no por intento.
X-Comges-Delivery-AttemptNúmero de intento. 1 en la primera entrega.

A estos se suman los headers extra que hayas configurado en la suscripción.

Verificar la firma

v1 = HMAC_SHA256(secreto, "{timestamp}.{cuerpoCrudo}")

Tres reglas que no se pueden saltar:

  1. Usá los bytes crudos del request, no el JSON re-serializado. Cualquier cambio de espaciado o de orden de claves rompe la comparación. Es el error número uno: el framework parsea el cuerpo por vos, vos volvés a serializarlo para firmar, y no coincide nunca. En Express necesitás express.raw; en ASP.NET Core, leer el Body antes del binder; en PHP, php://input.
  2. El timestamp entra en el material firmado. Sin él, cualquiera que capture una entrega podría reproducirla para siempre. Con él, podés rechazar todo lo que tenga más de N minutos (5 es un valor razonable) y la firma sólo sirve dentro de esa ventana.
  3. Compará en tiempo constante (crypto.timingSafeEqual, hmac.compare_digest, CryptographicOperations.FixedTimeEquals, hash_equals). Un === sobre strings filtra información por el tiempo de respuesta.

Es el mismo esquema que usa Stripe, así que si ya tenés código para eso, sirve tal cual.

webhooks-comges
import crypto from 'node:crypto'
import express from 'express'

const app = express()
const SECRETO = process.env.COMGES_WEBHOOK_SECRET

// Importante: body CRUDO. Con express.json() ya perdiste los bytes originales
// y la firma no va a coincidir nunca.
app.post('/webhooks/comges', express.raw({ type: 'application/json' }), (req, res) => {
  const firma = req.header('X-Comges-Signature') ?? ''
  const partes = Object.fromEntries(firma.split(',').map((p) => p.split('=', 2)))
  const t = Number(partes.t)
  const recibida = partes.v1 ?? ''

  // 1) Ventana de tolerancia: descarta reproducciones viejas.
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) {
    return res.status(400).send('timestamp fuera de ventana')
  }

  // 2) HMAC-SHA256 sobre "{t}.{cuerpoCrudo}".
  const esperada = crypto
    .createHmac('sha256', SECRETO)
    .update(`${t}.${req.body.toString('utf8')}`)
    .digest('hex')

  // 3) Comparación en tiempo constante.
  const a = Buffer.from(esperada, 'utf8')
  const b = Buffer.from(recibida, 'utf8')
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('firma inválida')
  }

  const evento = JSON.parse(req.body.toString('utf8'))

  // 4) Deduplicar: la entrega es at-least-once.
  if (yaProcesado(evento.id)) return res.sendStatus(200)

  // 5) Responder rápido; el trabajo pesado va aparte.
  encolarParaProcesar(evento)
  res.sendStatus(200)
})

Vector de prueba fijo

Para verificar tu implementación sin recibir nada, acá va un caso cerrado. Si tu código produce este mismo hexadecimal, tu verificación está bien; si no, el problema es tuyo y no de la entrega. Sirve tal cual como test unitario del receptor.

DatoValor
Secretowhsec_documentacion_vector_de_prueba
X-Comges-Timestamp1774704312
Cuerpo568 bytes UTF-8, una sola línea, sin salto final (el bloque de abajo).
Material firmado1774704312. + el cuerpo, concatenados sin nada en medio.
v1 esperado66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd
Header completoX-Comges-Signature: t=1774704312,v1=66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd

El cuerpo es el mismo ejemplo dte.aceptado de más arriba, pero tal como viaja: compacto, sin saltos de línea y sin espacios entre claves.

vector-de-prueba
SECRETO='whsec_documentacion_vector_de_prueba'
TIMESTAMP='1774704312'
CUERPO='{"id":"0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11","tipo":"dte.aceptado","version":1,"ocurridoEn":"2026-08-26T12:30:00Z","ambiente":"Produccion","empresaRut":"76665850-4","datos":{"documentoId":"8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88","tipoDte":33,"folio":1234,"trackId":987654321,"estadoSii":2,"estadoSiiNombre":"Aceptado","detalle":"Aceptado por el SII","codRef":null,"codRefNombre":null,"documentoReferenciado":null,"rutEmisor":"76665850-4","rutReceptor":"96790240-3","razonSocialReceptor":"Cliente Ejemplo S.A.","montoTotal":119000,"fechaEmision":"2026-08-26","extra":{}}}'

printf '%s' "$TIMESTAMP.$CUERPO" | openssl dgst -sha256 -hmac "$SECRETO" -r
# 66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd *stdin

Calculadora de firma

Pegá el secreto, el timestamp y el cuerpo crudo, y compará el v1 que esperamos con el que calcula tu código.

568 bytes

Header que esperamos

Deduplicación: la entrega es at-least-once

Garantizamos que el evento llega al menos una vez, no exactamente una vez. El mismo X-Comges-Event-Id puede llegar más de una vez —un reintento después de que tu respuesta se perdió en la red, un reintento manual desde el portal, un rescate de una entrega que quedó colgada—. Guardá los ids procesados y devolvé 200 sin volver a hacer nada si ya lo viste.

El id es estable entre intentos y viaja tanto en el header como en id del cuerpo. Lo que cambia entre intentos es X-Comges-Delivery-Attempt: ese sirve para loguear, no para deduplicar.

Tampoco asumas orden. Si un documento pasa a dte.enviado y enseguida a dte.aceptado, los dos avisos pueden llegar en cualquier orden. Usá datos.estadoSii para decidir, no el orden de llegada.

Qué responder

Tu respuestaQué hacemos
2xxEntrega exitosa. No hay más intentos.
408 o 429Reintentamos según la escalera. Son las dos únicas excepciones dentro de los 4xx.
Cualquier otro 4xxAgotamos la entrega de inmediato, sin consumir la escalera. Un 404 o un 401 no se arregla repitiendo el mismo POST: no tiene sentido castigarte 31 horas.
5xx, timeout o error de redReintentamos según la escalera.

Respondé rápido y procesá en segundo plano: el POST tiene un timeout de 10 segundos. No hagas el trabajo pesado dentro del handler: si tardás más, el intento cuenta como fallido y te vas a comer un duplicado cuando reintentemos. Guardamos siempre el cuerpo de tu respuesta —hasta 64 KB, recortado si te pasás— así que un mensaje de error legible en el cuerpo del 4xx te va a ahorrar tiempo cuando revises el historial.

Reintentos

7 intentos en total: la entrega inicial más 6 reintentos. En total cubre unas 31 horas, que es tiempo de sobra para enterarte de que tu endpoint está caído y levantarlo. Después de eso la entrega queda agotada y sólo se reintenta a mano desde el portal.

IntentoEspera desde el anteriorAcumulado desde el evento
1Inmediato
21 minuto1 min
35 minutos6 min
415 minutos21 min
51 hora1 h 21 min
66 horas7 h 21 min
724 horas≈ 31 horas

La escalera sólo corre para lo que tiene sentido reintentar. Un 5xx, un timeout o un error de red se reintentan completos. Un 4xx permanente —400, 401, 404— agota la entrega en el primer intento: repetir el mismo POST no lo va a arreglar. Las dos excepciones dentro de los 4xx son 408 (timeout) y 429(demasiadas peticiones), que sí escalan: los dos dicen “ahora no, probá después”.

Administrar desde el portal

Todo esto lo hace el cliente solo, sin escribirle a nadie, en Portal → Webhooks. Hace falta el permiso webhook:configurar.

Crear y editar suscripciones

URL https de destino, ambiente y descripción. La edición es en la misma pantalla del detalle, sin formularios paralelos.

Elegir qué eventos recibir

Del catálogo completo, agrupado por categoría, o el comodín * para recibir todos —incluidos los tipos que agreguemos después—.

Filtrar por tipo de DTE

Si tu ERP sólo procesa facturas, no tiene por qué ahogarse en boletas. Ver la sección de filtros.

Agregar headers extra

Los que necesite tu gateway o tu WAF —por ejemplo un Authorization propio— fijos para todas las entregas de esa suscripción.

Generar o fijar tu propio secreto, y rotarlo

Lo genera el sistema o lo traés vos. La rotación es inmediata y sin ventana de gracia: ver la sección del secreto.

Simular cualquier evento contra tu endpoint

Elegís qué evento del catálogo mandar (dte.aceptado, recepcion.documento_recibido, caf.folios_bajos, …), ves el cuerpo exacto antes de enviarlo y te devuelve el resultado del intento —status, milisegundos, error— en el momento, sin esperar ningún backoff. Así podés desarrollar cada rama de tu receptor sin esperar a que el hecho ocurra de verdad: los eventos simulados llegan con datos.extra.prueba en true.

Ver el historial de entregas

Cada intento con el JSON exacto que se mandó y la respuesta que devolviste: status, headers y cuerpo. Es el material para depurar tu verificación HMAC sin pedirnos nada.

Reintentar, de a una o en lote

Una entrega puntual, o todas las que cumplan un filtro de estado, tipo y fechas. El lote tiene un tope duro de 500 entregas por corrida.

Ver métricas

Entregadas, fallidas y pendientes, tasa de éxito y latencia de tu endpoint (promedio y percentiles), por período, por tipo de evento y por suscripción.

Filtros y headers extra

Filtro por tipo de DTE

Además de elegir qué eventos querés, podés acotar sobre qué documentos. El filtro es una lista de códigos SII —33,34,61, por ejemplo—. Vacío significa todos.

  • Se aplica además de la lista de eventos, no en su lugar: primero mira si el tipo de evento te interesa, después si el tipo de documento pasa el filtro.
  • Si el evento no trae tipoDte, el filtro no aplica y el evento se entrega igual. Es el caso de certificado.por_vencer —el día que se publique—: no hay documento que filtrar, y silenciar un aviso operativo por tener puesto un filtro de documentos sería el peor resultado posible.
  • El caso que resuelve: un ERP que sólo procesa facturas no tiene por qué recibir —ni verificar, ni encolar, ni descartar— cada boleta de un punto de venta con miles de emisiones por día.

Headers extra

Si tu endpoint vive detrás de un gateway que exige su propia autenticación, podés declarar headers fijos que se agregan a todas las entregas de esa suscripción —el caso típico es un Authorization: Bearer … de tu API gateway—.

  • No pueden pisar ningún header X-Comges-* ni Content-Type. Al guardar, uno de esos nombres se rechaza con un 400; al entregar, se ignora. Son los que sostienen la firma y la deduplicación: si pudieras sobrescribir X-Comges-Signature estarías anulando tu propia verificación, y si pudieras sobrescribir Content-Type romperías el parseo del cuerpo.
  • Máximo 10 headers, con nombres alfanuméricos y guiones ([A-Za-z0-9-]) y 1000 caracteres de tope en total.
  • Van en claro en el request: quedan visibles en el historial de entregas, igual que el resto de los headers. No los uses para nada que no puedas ver en tu propio panel.

El secreto de firma

  • Autogenerado o propio. El portal lo genera con prefijo whsec_, o traés el tuyo: mínimo 16 caracteres. Si ya manejás secretos en un vault, poné el que ya tenés y no toques nada más.
  • Se muestra una sola vez, al crearlo o al rotarlo. Después siempre viaja enmascarado (whsec_••••abcd). Guardalo apenas lo recibís: no hay forma de volver a verlo, sólo de rotarlo.
  • La rotación no tiene ventana de gracia. No conviven el secreto viejo y el nuevo: la entrega siguiente ya va firmada con el nuevo, y cualquier cosa que todavía verifique con el viejo va a rechazarla. Actualizá tu receptor antes de rotar, no después. Si podés, dejá el receptor aceptando los dos durante el despliegue y sacá el viejo cuando la rotación esté hecha.
  • El secreto nunca viaja en el request del webhook ni queda guardado en el historial de entregas. Sólo lo tenemos nosotros para firmar y vos para verificar.

Requisitos del endpoint de destino

La URL la elegís vos y la llama nuestro backend, así que la validamos con criterio estricto: al registrarla y otra vez antes de cada POST, porque el DNS puede cambiar en el medio.

  • https obligatorio. No aceptamos http.
  • Sin direcciones internas: nada de loopback, link-local, multicast ni rangos privados (RFC 1918, CGNAT, IPv6 ULA). Si tu host resuelve a una de esas, la entrega no sale.
  • Sin credenciales embebidas en la URL (https://usuario:clave@host). Autenticá con la firma HMAC, o con un header extra si tu gateway lo exige.
  • No seguimos redirects. Un 302 no se sigue: devolvé 2xx en la URL registrada.

Limitación conocida

dte.error_envio se notifica una sola vez por documento. Si el mismo sobre falla al subir otra vez, no volvés a recibir el aviso. Es deliberado —evita spamear mientras un sobre reintenta solo—, pero significa que ves el primer fallo y no los siguientes. Como ese evento no es terminal, el documento va a terminar en un estado final igual y ese sí te llega.

Esta guía es un tutorial. La referencia completa de la API pública vive en el API Reference. Ante cualquier diferencia, manda lo que diga ahí.