Webhooks
Un aviso firmado cuando una sincronización termina o un enlace de conexión se usa, sin sondear. El webhook nunca es load-bearing: el estado real siempre se puede consultar.
Connect entrega eventos por HTTP POST a los endpoints que registres: cada sincronización que termina y cada enlace de conexión que se consume. La alternativa es sondear GET /v1/syncs/{id}, que funciona igual de bien, solo que pagando la espera con llamadas tuyas.
Una regla gobierna todo el diseño: el webhook nunca es load-bearing. Si tu endpoint está caído, la sincronización igual terminó y la conexión igual quedó creada; la entrega se reintenta sola y el estado real vive siempre en la API. Trata cada evento como un aviso de que hay algo que mirar, nunca como la fuente del dato.
Para configurar webhooks por API, emite una clave con webhooks:write o autoriza ese permiso mediante
OAuth. Las consultas de endpoints y entregas requieren webhooks:read. Si necesitas ambas operaciones,
otorga ambos permisos: escritura no incluye lectura. Una credencial sin el permiso correspondiente
recibe 403 scope_not_granted, aunque pueda consultar otros sistemas de tu organización. Las claves
anteriores con permisos explícitos pueden ampliarse editando la clave activa como owner/admin o
emitiendo una nueva; OAuth requiere un nuevo
consentimiento. En el dashboard, administrar webhooks sigue disponible para owner y admin.
Eventos
| Evento | Cuándo dispara |
|---|---|
sync.succeeded | Un trabajo de sincronización terminó bien. Incluye los resultados parciales: el detalle por alcance viaja en outcomes. |
sync.failed | El trabajo terminó en fallo terminal: un error no reintentable, o se agotaron los reintentos internos. |
connect_session.consumed | Un enlace de conexión se usó con éxito y dejó una conexión creada o reautenticada. |
sii.honorarios.issued | Se confirmó la emisión de la boleta de honorarios. |
sii.honorarios.inconclusive | El resultado de la emisión requiere reconciliación. |
sii.honorarios.rejected | La solicitud de emisión fue rechazada. |
sii.honorarios.document_available | El documento de la boleta está disponible en Connect. |
Los eventos de honorarios incluyen request_id, connection_id, document_version y state. El payload no incluye RUT, montos ni el PDF. Consulta el recurso autorizado de la solicitud para obtener su estado y documento.
sync.partial se conserva por compatibilidad en REST y suscripciones anteriores, pero no se ofrece como evento disponible ni se emite actualmente: un parcial llega como sync.succeeded con el detalle en outcomes.
Cuerpo de un sync.succeeded:
{
"type": "sync.succeeded",
"timestamp": "2026-08-07T09:00:12.000Z",
"data": {
"job_id": "sjb_k2Rw81QpLm3N",
"connection": { "id": "conn_9tKfR2mQx4Vb", "connector": "sii", "label": "Comercial Aurora SpA" },
"alcances": ["rcv", "boletas"],
"periodo": "2026-07",
"status": "succeeded",
"records_synced": 214,
"last_error": null,
"outcomes": [
{ "alcance": "rcv", "status": "ok", "records_synced": 180 },
{ "alcance": "boletas", "status": "ok", "records_synced": 34 }
],
"trigger": "scheduled",
"request_id": "req_..."
}
}Un trabajo donde un alcance falló y otro no sigue siendo sync.succeeded: el trabajo hizo progreso real. La mezcla se lee en outcomes, donde cada alcance declara su propio status (ok, partial o failed) y, cuando falló, un error con un código del catálogo:
"outcomes": [
{ "alcance": "rcv", "status": "ok", "records_synced": 180 },
{ "alcance": "boletas", "status": "failed", "records_synced": 0, "error": "timeout" }
]Algunos conectores agregan a un outcome los campos incompletos (cuántos registros quedaron a medio traer) y completo (si el período quedó cerrado); solo aparecen cuando el conector los reporta.
El sync.failed tiene la misma forma, con records_synced en null, outcomes vacío y el código del error en last_error:
{
"type": "sync.failed",
"timestamp": "2026-08-07T09:05:30.000Z",
"data": {
"job_id": "sjb_...",
"connection": { "id": "conn_9tKfR2mQx4Vb", "connector": "sii", "label": "Comercial Aurora SpA" },
"alcances": ["rcv", "boletas"],
"periodo": "2026-07",
"status": "failed",
"records_synced": null,
"last_error": "upstream_error",
"outcomes": [],
"trigger": "scheduled",
"request_id": "req_..."
}
}Tanto last_error como el error de cada outcome llevan códigos del catálogo, nunca texto libre: el payload de un webhook jamás incluye documentos sincronizados, campos de credencial ni mensajes crudos del sistema externo. El request_id es el mismo que quedó en la bitácora, así que correlacionar el evento con su ejecución es una búsqueda exacta.
El evento connect_session.consumed avisa que un enlace hosted se completó:
{
"type": "connect_session.consumed",
"timestamp": "2026-07-28T12:00:00.000Z",
"data": {
"session": { "id": "cs_7f3ab9c1d2e4f5061728" },
"connection": { "id": "conn_9tKfR2mQx4Vb", "connector": "sii" },
"verified": true,
"consumed_at": "2026-07-28T12:00:00.000Z"
}
}verified admite tres valores: true cuando la verificación contra la fuente corrió y pasó; false cuando corrió y falló, aunque la conexión existe igual con la credencial entregada; null cuando el conector no declara una tool de verificación y no había nada que correr. El payload identifica qué enlace se usó y qué conexión resultó, y nada más: nunca el token del enlace ni ningún campo de lo que la persona escribió.
Crear un endpoint
curl -X POST https://connect.emisso.ai/api/v1/webhooks \
-H "Authorization: Bearer connect_sk_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.tu-producto.cl/hooks/connect",
"event_types": ["sync.succeeded", "sync.failed"],
"connection_id": "conn_9tKfR2mQx4Vb"
}'Respuesta (201):
{
"data": {
"id": "whk_...",
"url": "https://api.tu-producto.cl/hooks/connect",
"event_types": ["sync.succeeded", "sync.failed"],
"connection_id": "conn_9tKfR2mQx4Vb",
"enabled": true,
"disabled_at": null,
"last_success_at": null,
"created_at": "2026-08-07T12:00:00.000Z",
"secret": "whsec_..."
},
"meta": { "request_id": "req_..." }
}El secreto aparece una sola vez
secret se devuelve únicamente al crear el endpoint (y al rotarlo). Se guarda cifrado y ninguna lectura posterior lo incluye: GET /v1/webhooks devuelve el endpoint sin él. Si lo pierdes, rota el secreto.
Reglas del alta:
event_typesvacío u omitido suscribe a todos los eventos, presentes y futuros. Listar tipos explícitos congela la suscripción a esos.connection_ides opcional y acota el endpoint a los eventos de esa conexión. Debe ser una conexión de tu organización.- La
urldebe serhttpsy resolver a una dirección pública. Direcciones internas o no resolubles se rechazan con422 invalid_webhook_url. - Un endpoint solo recibe eventos posteriores a su creación; registrarlo no re-entrega historia.
- Los webhooks son una función de plan: sin ella el alta responde
403 feature_not_in_plan.
La misma alta existe sin código en el dashboard, en connect.emisso.ai/webhooks. Para editar por API: PATCH /v1/webhooks/{id} acepta url, event_types y enabled; DELETE /v1/webhooks/{id} deshabilita (nunca borra: las entregas históricas se conservan).
Probar y diagnosticar en el dashboard
Abre un endpoint en Webhooks y selecciona Probar. La prueba requiere un endpoint habilitado y un plan con webhooks; puedes solicitar una por endpoint cada 60 segundos. Entra a la misma cola y usa la misma firma y política de reintentos que una entrega de producción.
{
"type": "webhook.test",
"timestamp": "2026-09-08T12:00:00.000Z",
"data": {
"test": true,
"message": "Evento de prueba de Emisso Connect",
"endpoint_id": "whk_..."
}
}webhook.test solo se envía al endpoint elegido. No es suscribible, no se distribuye a los demás endpoints y no representa una sincronización real. Tu receptor debe reconocerlo antes de ejecutar lógica de negocio.
Pausar un endpoint deja de generar entregas para eventos nuevos. Las entregas ya en cola continúan sus intentos. Los eventos ocurridos durante la pausa no se recuperan automáticamente.
Recuperar un evento anterior
En las entregas de un endpoint, un propietario o administrador puede abrir Recuperar un evento anterior e indicar su ID whev_…. Esta acción permite entregar un evento existente a un endpoint creado después del evento. Requiere un plan con webhooks, un endpoint habilitado y filtros compatibles con el tipo de evento y su conexión.
La recuperación conserva el ID, el contenido y la fecha del evento original. Si ya existe una entrega para ese evento y endpoint, abre su historial sin duplicarla ni reiniciar sus intentos; una entrega fallida puede reintentarse desde el inspector. La solicitud de recuperación queda atribuida al usuario en la auditoría. Un resultado encolado no confirma recepción: revisa el HTTP y los intentos de la entrega.
En honorarios, recuperar sii.honorarios.issued o sii.honorarios.document_available no vuelve a emitir la boleta ni accede al SII. El segundo evento confirma la disponibilidad del documento; solo una respuesta 2xx de su receptor confirma su entrega.
En Entregas, selecciona una fila para ver los datos del evento y el historial de intentos. Cada intento observado presenta su hora, HTTP, duración y un motivo seguro cuando falla. Si un error ocurre antes de construir una petición (por ejemplo, al abrir el secreto o validar DNS) no se inventa un timestamp ni una duración HTTP. Las entregas anteriores a esta instrumentación pueden carecer de historial detallado.
Los datos del evento son el payload guardado por Connect. El timestamp registrado corresponde a la petición de ese intento; esta vista no es una captura HTTP histórica. No guardamos el cuerpo de respuesta del receptor ni mostramos firmas o secretos. Un sync.failed recibido con HTTP 200 significa que tu endpoint recibió correctamente el aviso de una sincronización fallida.
| Resultado de recepción | Qué revisar |
|---|---|
2xx | La entrega terminó. Deduplica por webhook-id antes de procesar el evento. |
3xx | Configura la URL final: Connect no sigue redirects. |
400, 404, 422 | Revisa ruta, parser y manejo del tipo de evento. Estos rechazos son terminales. |
401, 403 | Revisa el secreto, la firma sobre los bytes crudos y las reglas de acceso del receptor. |
408, 429 | Revisa el tiempo de respuesta o la capacidad; Connect reintentará. |
5xx | Revisa los errores del receptor; Connect reintentará. |
| Timeout o error de red | Revisa disponibilidad, DNS, certificado TLS y acceso público por HTTPS. |
Para probar un receptor que corre en tu computador, publícalo temporalmente mediante un túnel HTTPS y registra esa URL pública. Verifica la firma sobre el cuerpo crudo con el ejemplo siguiente, responde rápido con 2xx y procesa el trabajo de forma asíncrona. Una URL de localhost o una IP privada no es un destino válido para Connect. Usa datos sintéticos para comprobar la integración.
Verificar la firma
Toda entrega llega firmada con el esquema Standard Webhooks. Tres headers acompañan el cuerpo:
| Header | Contenido |
|---|---|
webhook-id | El id del evento (whev_...). Es estable entre reintentos y entre endpoints: úsalo como clave de deduplicación. |
webhook-timestamp | El instante del intento de entrega, en segundos Unix. Cada reintento se firma de nuevo con el suyo. |
webhook-signature | Una o más firmas separadas por espacio, cada una con la forma v1,<base64>. Hay más de una solo durante una rotación de secreto. |
Lo que se firma es la cadena id.timestamp.body, donde body son los bytes exactos del cuerpo recibido. El algoritmo: HMAC-SHA256, con clave igual al secreto sin su prefijo whsec_ decodificado de base64; el digest viaja en base64 con el prefijo de versión v1,. Este verificador espeja el del servidor:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(input: {
secret: string; // whsec_... tal como lo entregó POST /v1/webhooks
id: string; // header webhook-id
timestamp: number; // header webhook-timestamp, en segundos
rawBody: string; // el cuerpo crudo recibido, sin parsear ni re-serializar
header: string; // header webhook-signature
}): boolean {
// 1. Tolerancia de reloj: 300 segundos. Como cada reintento trae su propio
// timestamp, un reintento tardío nunca queda fuera de la ventana.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - input.timestamp) > 300) return false;
// 2. La clave HMAC es el secreto sin el prefijo whsec_, decodificado de base64.
const key = Buffer.from(input.secret.slice("whsec_".length), "base64");
// 3. Se firma `${id}.${timestamp}.${body}`; el digest va en base64 tras "v1,".
const expected = Buffer.from(
`v1,${createHmac("sha256", key).update(`${input.id}.${input.timestamp}.${input.rawBody}`).digest("base64")}`,
);
// 4. El header puede traer varias firmas separadas por espacio (rotación de
// secreto): la entrega es válida si cualquiera calza, en tiempo constante.
return input.header.split(" ").some((candidate) => {
const c = Buffer.from(candidate);
return c.length === expected.length && timingSafeEqual(c, expected);
});
}Verifica sobre los bytes crudos
El cuerpo se envía como JSON canónico: claves ordenadas alfabéticamente, sin espacios. La firma cubre exactamente esos bytes. Si haces JSON.parse y vuelves a serializar antes de verificar, los bytes cambian y la firma deja de calzar. Lee el cuerpo crudo, verifica, y recién entonces parsea. Los ejemplos de esta página van indentados solo para leerse.
Reintentos y reenvío
Una entrega cuenta como exitosa cuando tu endpoint responde 2xx dentro de 10 segundos. Una redirección no se sigue y cuenta como rechazo. Ante un fallo, la política distingue dos clases:
- Se reintenta lo que puede sanar solo: errores de red, respuestas
5xx,408y429. - Es terminal cualquier otro
4xx: tu endpoint está rechazando el evento y volver a golpear no lo arregla.
Los reintentos son hasta 6 intentos en total, con esperas crecientes tras cada fallo: 1 minuto, 5 minutos, 30 minutos, 2 horas y 6 horas. El bus de entregas corre cada minuto, así que cada espera es un piso, no un instante exacto. Con 5 fallos terminales consecutivos el endpoint se deshabilita solo (enabled: false, con disabled_at poblado); una entrega exitosa reinicia el contador, y rehabilitarlo es un PATCH con {"enabled": true} o un clic en el dashboard.
Las entregas de un endpoint se listan con su resultado:
curl "https://connect.emisso.ai/api/v1/webhooks/whk_.../deliveries?limit=5" \
-H "Authorization: Bearer connect_sk_..."Respuesta (200):
{
"data": [
{
"id": "whd_...",
"event_type": "sync.succeeded",
"status": "delivered",
"attempts": 1,
"response_status": 200,
"error": null,
"created_at": "2026-08-07T09:00:14.000Z",
"updated_at": "2026-08-07T09:00:15.000Z"
},
{
"id": "whd_...",
"event_type": "sync.failed",
"status": "failed",
"attempts": 6,
"response_status": 500,
"error": "HTTP 500",
"created_at": "2026-08-06T21:10:02.000Z",
"updated_at": "2026-08-07T05:41:10.000Z"
}
],
"pagination": { "cursor": null, "hasMore": false }
}status recorre pending, delivering, delivered y failed; limit acepta de 1 a 100, con 50 por defecto. Una entrega que quedó en failed se reenvía a mano:
curl -X POST "https://connect.emisso.ai/api/v1/webhooks/whk_.../deliveries/whd_.../resend" \
-H "Authorization: Bearer connect_sk_..."Respuesta (200):
{ "data": { "resent": true } }El reenvío admite entregas terminales (failed o delivered), vuelve a ponerlas en pending con el presupuesto de intentos en cero y conserva el historial previo. El bus las toma en su próxima pasada. El webhook-id no cambia: tu deduplicación lo verá como el mismo evento, que es exactamente lo que es.
Rotar el secreto
curl -X POST https://connect.emisso.ai/api/v1/webhooks/whk_.../roll-secret \
-H "Authorization: Bearer connect_sk_..."Respuesta (200):
{
"data": { "secret": "whsec_..." },
"meta": { "request_id": "req_..." }
}Durante las 24 horas siguientes conviven los dos secretos: cada entrega lleva en webhook-signature una firma por cada secreto vigente, separadas por espacio. El verificador de arriba ya lo contempla (acepta si cualquiera calza), así que la rotación no exige coordinar un despliegue: publica el secreto nuevo en tu endpoint dentro de la ventana y el viejo muere solo al vencer.
Próximos pasos
- La superficie de control completa, con
connect_sessions, syncs y salud: API de control. - El evento
connect_session.consumeden su contexto, con el ciclo de vida del enlace: enlace hosted. - Correlacionar un
request_idcon su ejecución: la bitácora. - Qué responde Connect cuando algo se rechaza y qué hacer en cada caso: errores.
Conecta a tus clientes
El caso plataforma, un producto que conecta a muchos clientes finales, con el modelo de datos, el alta embebida y los topes que importan a escala.
Manejo de errores
Cada error de Connect llega con un código estable, una acción sugerida y un correlativo de soporte. Esta guía enseña a manejarlos; el catálogo completo vive en la referencia.