Webhooks
Recibe por push cada cambio de estado SII de tus documentos, firmado con Notta-Signature y con reintentos durables.
Registras una URL https, Notta le hace POST cada vez que cambia el estado SII de uno de tus documentos. Es el camino de push: te enteras del veredicto del SII sin preguntar.
Toda mutación exige una API key con el scope webhook:write: registrar, rotar el secret, probar la entrega y borrar. Listar exige webhook:read. Son dos llaves distintas: webhook:write no incluye webhook:read. Créalas en Ajustes, API keys. Este endpoint es solo Bearer: no acepta la sesión del navegador.
Suscripción
La respuesta trae el secret de firma y es la única vez que lo vas a ver:
El header Idempotency-Key es obligatorio: sin él la respuesta es 400 idempotency_key_missing. Reintentar con el mismo valor devuelve la misma suscripción con 200 en vez de crear otra, y la clave es la única identidad que miramos: si repites la llamada con una clave nueva sobre la misma URL, quedan dos suscripciones y tu endpoint recibe cada evento dos veces, firmado con dos secrets distintos. Guarda la clave que usaste, o revisa el listado antes de repetir. La URL tiene que ser https pública. Una que apunte a localhost, a un rango privado, al servicio de metadata de tu nube o a un puerto propio se rechaza con 422 invalid_webhook_url, y esa comprobación se repite en cada entrega, no solo al registrar.
Ambiente
Cada suscripción vale para un ambiente: el de la API key que la creó. No es un campo del cuerpo, porque el ambiente no lo declara quien llama. Una key ntt_cert_… registra un endpoint de certificación y ese endpoint nunca recibe un documento de producción, ni al revés. Certificación y producción son secuencias de folios independientes del SII, así que el mismo folio existe una vez en cada una.
La consecuencia práctica: si emites en los dos ambientes, registras dos suscripciones. Cada evento repite el ambiente en data.sii_env, y GET /webhooks solo lista las del ambiente de la key con la que preguntas. Pedir por id una suscripción del otro ambiente devuelve 404, aunque sea de tu misma empresa.
Tipos de evento
| Evento | Cuándo |
|---|---|
dte.emitted | El DTE fue firmado y encolado al SII (ya tiene Track ID) |
dte.status_changed | El estado SII cambió (p. ej. sending → EPR). Incluye los intermedios |
dte.accepted | El SII aceptó el DTE (EPR, RPR; terminal) |
dte.rejected | El SII rechazó el DTE. RCH es el rechazo del documento (el sobre se procesó y el DTE quedó fuera, el caso más común); RFR (firma), RCT (carátula) y RSC (schema) rechazan el envío completo. Ver Catálogo de errores |
dte.accepted y dte.rejected viajan siempre acompañados de su dte.status_changed: son la misma transición contada dos veces, una neutra y una con veredicto. Si te suscribes a los tres, recibes las dos entregas.
Los documentos que entran por el Respaldo del SII (una migración desde otro facturador) no producen ninguna entrega: el Servicio ya los había aceptado en su momento y no hay transición en vivo que avisar. Si importas años de historia, tu endpoint no ve nada de eso.
Cuántas entregas produce una emisión
Medido contra el SII el 25 de agosto de 2026, sobre una factura 33 que salió aceptada: cuatro transiciones en 59 segundos.
O sea cuatro entregas si estás suscrito a todo: la de sending, la del estado intermedio, y las dos del veredicto.
Cuál sea el estado intermedio varía, y tu handler no debe depender de eso. Depende de en qué punto de su cascada esté el SII cuando le preguntamos. El -11 del ejemplo es el más frecuente (el código con el que el Servicio responde mientras todavía no registra el envío: por él pasa el 78% de los documentos que llegan a un estado terminal), pero también aparecen SOK, CRT, FOK, PDR y PRD. Una segunda factura 33 aceptada, medida el 26 de agosto de 2026, pasó por SOK y produjo exactamente las mismas cuatro entregas. Y si el SII reporta más de un intermedio, hay una entrega más por cada uno: lo estable no es el número total, sino que dte.emitted llega una vez y el veredicto llega como par. Ramifica por data.terminal y data.categoria, nunca por el string de data.sii_status.
Si solo te interesa el desenlace, suscríbete a dte.accepted y dte.rejected y no vas a ver un solo intermedio. El filtro es la suscripción, no el payload: suscribirse a dte.status_changed "por si acaso" te trae el triple de tráfico del que necesitas.
Ese recorte tiene un costo que conviene tener presente: un documento cuyo desenlace es requiere_accion viaja como dte.status_changed y nunca como dte.accepted ni dte.rejected. Es el caso de stuck (el SII no dio veredicto dentro de la ventana del poll) y de sin_permiso_sii (el RUT del certificado no está enrolado para consultar). Con una suscripción de solo desenlace, esos documentos quedan sin aviso, y son justamente los que necesitan que alguien intervenga. Súmale dte.status_changed, o repasa periódicamente con GET /api/v1/dtes?polled_since= lo que no cerró.
Cuánto tarda en llegar
El veredicto del SII (dte.accepted, dte.rejected, y el dte.status_changed que los acompaña) se despacha apenas se persiste la transición: no espera a ningún barrido. Medido en producción el 26 de agosto de 2026, sobre una factura 33 en certificación: alrededor de 2 segundos entre la transición y nuestro POST. Los desenlaces que exigen intervención (stuck, sin_permiso_sii) salen por el mismo camino inmediato.
Los eventos intermedios (dte.emitted y los dte.status_changed de estados que todavía no son desenlace) los recoge un barrido que corre cada minuto, así que llegan en menos de un minuto.
En el peor caso, si el despacho inmediato no sale o el lote de entregas es grande, una entrega puede esperar un par de minutos antes de que otra corrida la tome. Los tiempos son hasta que sale nuestro POST: lo que tarde tu servidor en responder corre por tu cuenta y cuenta para el timeout de 10 segundos de la entrega.
Payload
Cada entrega es un POST con Content-Type: application/json y User-Agent: Notta-Webhooks/1.0. Si tu endpoint vive detrás de un WAF o de una allowlist, ese es el agente que va a golpear.
Los tres últimos campos son los de decisión, y son los mismos que publica el carril de pull (ver Estados de un DTE):
terminal: si hay veredicto. Corta tu espera por aquí, nunca porsii_status === "EPR", porque un rechazo también es final.categoria: cómo terminó, o por qué no terminó (aceptado,rechazado,en_proceso,requiere_accion,archivistico).accion: qué hacer al respecto (ninguna,esperar,reemitir,accion_en_sii,reintentar_consulta,contactar_soporte).
Un código que el SII invente y que Notta todavía no reconozca llega igual, clasificado con el default conservador (terminal: false, accion: "contactar_soporte"), nunca como aceptado.
El sobre no lleva RUT, ni razón social, ni montos: es deliberado, para no mandar datos personales a un tercero en cada transición. Si necesitas el detalle del documento, pídelo con GET /api/v1/dtes/{id} usando el data.dte_id.
Verificación de firma
Cada entrega incluye un header Notta-Signature con la forma t=<unix>,v1=<hex>. El HMAC-SHA256 se calcula sobre ${t}.${cuerpo crudo}, firmado con tu webhook secret, y la tolerancia de tiempo es de 5 minutos. El timestamp entra en el material firmado: sin él, una entrega capturada sirve para siempre.
Firma el cuerpo crudo, tal como llegó. Si tu framework parsea el JSON y vuelves a serializarlo, un espacio de diferencia rompe la firma.
De dónde sale el secret
El webhook secret se entrega una sola vez, en la respuesta de POST /api/v1/webhooks. Guárdalo en tu gestor de secretos en ese momento: no es recuperable después, solo rotable.
La rotación devuelve el valor nuevo una sola vez y el anterior deja de firmar de inmediato. Despliega el secret nuevo en tu verificador antes de rotar, o vas a rechazar entregas legítimas.
Probar la entrega
Encola una entrega contra tu URL para que verifiques firma y conectividad. Responde 202 con {"delivery_id": "<uuid>", "event_id": "evt_…", "status": "pending"}. El event_id es el mismo id que vas a ver en el sobre que te llegue.
Si la suscripción está apagada (active: false) responde 409 webhook_endpoint_disabled y no encola nada, con el porqué en detail.disabled_reason. Arreglar tu servidor no la revive: una suscripción apagada no se reactiva, así que bórrala, regístrala de nuevo y despliega el secret nuevo en tu verificador antes de volver a probar.
Esa entrega viaja siempre con type: "dte.status_changed", aunque tu endpoint no esté suscrito a ese evento: prueba el transporte, no el ruteo. Si tu handler ramifica por type, contempla ese caso o vas a descartar tu propia prueba pensando que falló. Trae data.sii_status: "TEST", el dte_id en el UUID nulo y folio: 0, porque no hay documento detrás. Y como TEST no es un código del SII, llega clasificado como desconocido (terminal: false, accion: "contactar_soporte"): no lo trates como una alerta.
Reintentos de entrega
Una respuesta 2xx es éxito. Cualquier otro código, un timeout o un error de red reintentan. Tienes 10 segundos para responder: pasado ese plazo cortamos la conexión y el intento cuenta como fallido, aunque tu handler siga trabajando. Si el procesamiento es pesado, responde 2xx primero y encólalo de tu lado. Los redirects no se siguen: si tu URL responde 301 o 302, la entrega cuenta como fallida, así que registra la URL final.
| Intento | Cuándo |
|---|---|
| 1 | Apenas se produce la transición |
| 2 | 1 minuto después |
| 3 | 30 minutos después |
| 4 | 2 horas después |
| 5 | 12 horas después |
Cinco intentos en total. Si el quinto falla, la entrega queda agotada y no se vuelve a intentar. Si tres entregas agotadas consecutivas son del mismo endpoint, lo apagamos: pasa a active: false, deja de recibir eventos y sale un correo con el motivo al dueño de la organización. Apagarlo en silencio te dejaría sin eventos y sin forma de distinguirlo de un día sin documentos.
Ese correo va a la casilla del dueño de la cuenta, que puede no ser quien integra: si el aviso tiene que llegarte a ti, la señal que sí controlas es GET /api/v1/webhooks (mira active y disabled_reason) más el código de respuesta de tu propio endpoint.
Un endpoint apagado se ve en GET /api/v1/webhooks, con disabled_at y disabled_reason. Para volver a recibir, arregla tu servidor y registra la suscripción de nuevo. La suscripción apagada no se reactiva sola ni se borra sola: bórrala con DELETE /api/v1/webhooks/{id} o vas a verla en el listado junto a la nueva.
Orden de llegada
Las entregas salen en paralelo, así que pueden llegar desordenadas y repetidas. Un dte.accepted puede aparecer antes que su dte.emitted, y un reintento puede duplicar algo que ya procesaste.
Dos reglas para tu handler:
- Ordena por
data.observed_at, que es el instante de la transición, y descarta lo que sea más viejo que el último estado que ya conoces de ese documento. No uses la hora de llegada. - Sé idempotente por el
iddel evento. El mismoidpuede llegar más de una vez y significa exactamente el mismo hecho.
Si una entrega no llega
Los webhooks son el camino de push, no el único camino. Cuando un endpoint se apaga, o una entrega agota sus cinco intentos porque tu servidor estuvo caído, esos eventos no se reponen: hay que ir a buscarlos.
Para eso está el pull, y por eso no lo retiramos:
GET /api/v1/dtes?polled_since=<iso>te trae los documentos cuyo estado se movió desde ese instante. Es la forma de ponerte al día después de una caída.GET /api/v1/dtes/{id}/eventste da la historia completa de transiciones de un documento, con la glosa del SII.
Guarda el data.observed_at del último evento que procesaste con éxito y úsalo como polled_since al volver. Sin ese camino, un webhook caído es pérdida silenciosa.
Retención de las entregas
El historial de entregas es interno: ninguna API te lo devuelve. GET /api/v1/webhooks lista tus suscripciones, no los POST que salieron. La única copia que puedes leer eres tú, así que registra en tu servidor el id del evento y el resultado de cada entrega que recibas.
Del lado nuestro, el registro de una entrega terminal (entregada o agotada) se conserva 90 días y después se purga, o sea que más atrás no queda rastro que podamos revisar contigo. La fuente de verdad del estado de un documento sí es consultable y no caduca: GET /api/v1/dtes/{id}/events.
Listar y borrar
El listado devuelve un objeto con la clave webhooks, nunca el secret:
El borrado responde 204 sin cuerpo y es definitivo: se lleva TODO el registro de entregas de ese endpoint, entregadas incluidas, no sólo las pendientes (verificado en producción: borrar una suscripción con 23 entregas dejó cero filas). Como ese registro es interno y ninguna API lo publica, lo que quieras conservar tiene que estar ya en los logs de tu servidor. Registrar la misma URL después crea otra suscripción, con otro secret.
Para agentes LLM
Los cinco endpoints de /webhooks están fuera del catálogo MCP a propósito. Registrar una URL de callback redirige el flujo de documentos de tu empresa hacia un tercero, y esa es una decisión de infraestructura de la persona que integra, no del agente que consume. Un agente que necesita seguir un documento usa el pull (GET /dtes/{id}/events, GET /dtes?polled_since=), que le da la misma información sin poder redirigir nada.
Próximos pasos
- Estados de un DTE: qué significa cada
sii_status, y cómo se decide conterminal,categoriayaccion. - Referencia API de DTEs:
GET /dtes/:id/events, el feed cronológico de transiciones con la glosa del SII, y el camino para ponerte al día si una entrega no llegó. - Catálogo de errores: los rechazos SII que vas a recibir en
dte.rejected. - Agentes: patrones para que un agente LLM reaccione a los eventos.
- Límites de tasa: el patrón de backoff para las llamadas que hagas tú a la API.
Última actualización