Nodda Docs

Webhooks

En lugar de consultar el API cada cierto tiempo, registra un endpoint HTTP y Nodda te notifica cada vez que termina una sincronización. El flujo típico es:

  1. Nodda ejecuta el sync en el horario configurado (o bajo demanda).
  2. Al finalizar emite un evento (sync.succeeded, sync.partial o sync.failed) hacia tu endpoint.
  3. Tu servicio recibe el evento, verifica la firma y llama al API de lectura para obtener los datos frescos.

Los webhooks son solo notificaciones: no contienen datos financieros ni PII. La carga es mínima y la verificación de firma es obligatoria.

Registra y administra tus endpoints desde el panel: app.nodda.cl → Webhooks.


Catálogo de eventos

EventoCuándo se emite
sync.succeededla corrida terminó sin errores
sync.partialla corrida terminó pero algún scope tuvo problemas no críticos
sync.failedla corrida falló completamente

Estructura del payload

El cuerpo de cada entrega es JSON con esta forma:

{
  "type": "sync.succeeded",
  "timestamp": "2026-06-22T14:05:33.000Z",
  "data": {
    "syncRunId": "9b8c7d6e-5f4a-3b2c-1d0e-aabbccddee01",
    "organizationId": "11111111-2222-3333-4444-555555555555",
    "connectionId": "3f1c9e2a-7b44-4d51-9a0e-1c2d3e4f5a6b",
    "status": "succeeded",
    "scopes": ["movimientos"],
    "scopeOutcomes": [
      { "scope": "movimientos", "status": "ok", "recordsFound": 42, "durationMs": 1234, "period": "2026-05" }
    ]
  }
}

period es el periodo (YYYY-MM) sincronizado para ese scope, o null en scopes point-in-time (ej. saldos). Úsalo para llamar al read endpoint del scope con ?period=. durationMs — number. Wall-clock milliseconds this scope took during the sync run.

En sync.failed (y algunos sync.partial), data incluye además errorCode:

{
  "type": "sync.failed",
  "timestamp": "2026-06-22T14:05:33.000Z",
  "data": {
    "syncRunId": "9b8c7d6e-5f4a-3b2c-1d0e-aabbccddee02",
    "organizationId": "11111111-2222-3333-4444-555555555555",
    "connectionId": "3f1c9e2a-7b44-4d51-9a0e-1c2d3e4f5a6b",
    "status": "failed",
    "scopes": ["saldos"],
    "errorCode": "connector.auth_failed"
  }
}

Headers de la entrega

HeaderValor
content-typeapplication/json
webhook-idUUID único de la entrega (úsalo para deduplicar)
webhook-timestampUnix epoch en segundos (entero) del momento de entrega
webhook-signaturefirma v1,<base64 HMAC-SHA256> (ver abajo)

Verificar la firma

Nodda implementa la especificación Standard Webhooks. La firma se computa sobre:

HMAC-SHA256( decoded_secret, "<webhook-id>.<webhook-timestamp>.<rawBody>" )

donde:

  • decoded_secret es la clave base64-decodificada del secreto (prefijo whsec_ eliminado antes de decodificar).
  • <rawBody> es el cuerpo crudo tal cual llegó — no lo re-serialices.
  • El resultado se codifica en base64 y se prefija con v1,.

Con la librería oficial (recomendado)

Instala svix o standardwebhooks:

npm install standardwebhooks
import { Webhook } from "standardwebhooks";
 
const wh = new Webhook("whsec_<tu-secreto>");
 
// En tu handler (Express, Next.js, etc.)
export async function POST(req: Request) {
  const rawBody = await req.text();
  const headers = {
    "webhook-id": req.headers.get("webhook-id") ?? "",
    "webhook-timestamp": req.headers.get("webhook-timestamp") ?? "",
    "webhook-signature": req.headers.get("webhook-signature") ?? "",
  };
 
  try {
    const payload = wh.verify(rawBody, headers);
    // payload.type === "sync.succeeded" | "sync.partial" | "sync.failed"
    // payload.data.connectionId, payload.data.syncRunId, …
    return new Response("ok");
  } catch {
    return new Response("Unauthorized", { status: 401 });
  }
}

Verificación manual (Node.js)

Si prefieres no instalar una librería adicional:

import { createHmac, timingSafeEqual } from "node:crypto";
 
export function verifyWebhook(
  rawBody: string,
  headers: { "webhook-id": string; "webhook-timestamp": string; "webhook-signature": string },
  secret: string,          // "whsec_…"
): boolean {
  // 1. Verificar timestamp (rechaza replays de más de 5 minutos)
  const ts = Number(headers["webhook-timestamp"]);
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
 
  // 2. Decodificar el secreto (eliminar prefijo whsec_, decodificar base64)
  const raw = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
  const key = Buffer.from(raw, "base64");
 
  // 3. Calcular firma esperada
  const toSign = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${rawBody}`;
  const expected = createHmac("sha256", key).update(toSign).digest("base64");
  const expectedSig = `v1,${expected}`;
 
  // 4. Comparar de forma timing-safe
  const received = headers["webhook-signature"].split(" ").find(s => s.startsWith("v1,")) ?? "";
  const a = Buffer.from(expectedSig);
  const b = Buffer.from(received.padEnd(expectedSig.length));
  return a.length === b.length && timingSafeEqual(a, b);
}

Siempre usa comparación timing-safe (timingSafeEqual) y verifica el timestamp. Rechaza cualquier entrega con más de 5 minutos de antigüedad para mitigar ataques de replay.


Contrato de reintentos

Si tu endpoint no responde con un código 2xx, Nodda reintenta la entrega hasta 4 veces con la siguiente planificación fija:

IntentoRetraso acumulado
1 (inmediato)
2+1 min
3+30 min
4+2 h

Tras agotar los 4 intentos la entrega queda en estado failed. Si un endpoint acumula demasiadas fallas consecutivas, Nodda lo desactiva automáticamente — puedes reactivarlo desde el panel.

Diseña tu handler para ser idempotente. Usa el header webhook-id para deduplicar: una misma entrega puede llegar más de una vez. Responde 2xx lo antes posible y procesa en background — Nodda no impone un timeout largo pero un handler lento es frágil.

El orden de entrega entre eventos no está garantizado. Si un sync rápido y uno lento se solapan, puedes recibir el evento del segundo antes que el del primero.


Registrar un endpoint

Desde el panel

  1. Ve a app.nodda.cl → Webhooks → Nuevo endpoint.
  2. Introduce la URL de tu handler (HTTPS).
  3. Selecciona los tipos de evento que quieres recibir (sync.succeeded, sync.partial, sync.failed).
  4. Copia el secreto whsec_…se muestra solo una vez.

Via API REST

curl -X POST "https://app.nodda.cl/api/webhooks" \
  -H "Authorization: Bearer nodda_sk_tu_token" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/nodda",
    "event_types": ["sync.succeeded", "sync.partial", "sync.failed"]
  }'

Respuesta (201):

{
  "id": "whe_a1b2c3d4-0000-4000-8000-aabbccddeeff",
  "url": "https://your-app.example.com/webhooks/nodda",
  "event_types": ["sync.succeeded", "sync.partial", "sync.failed"],
  "secret": "whsec_AbCdEfGhIjKlMnOpQrStUvWx",
  "enabled": true,
  "created_at": "2026-06-22T14:00:00.000Z"
}

El campo secret solo aparece en la respuesta de creación y al rotar el secreto.

Rotar el secreto

curl -X POST "https://app.nodda.cl/api/webhooks/whe_a1b2c3d4-0000-4000-8000-aabbccddeeff/roll-secret" \
  -H "Authorization: Bearer nodda_sk_tu_token"

Esto genera un nuevo whsec_…, invalida el anterior y lo devuelve en la respuesta. Actualiza tu handler antes de rotar en producción.


Ver entregas y reintentar

# Listar entregas de un endpoint
curl "https://app.nodda.cl/api/webhooks/whe_a1b2c3d4-0000-4000-8000-aabbccddeeff/deliveries" \
  -H "Authorization: Bearer nodda_sk_tu_token"
 
# Reintentar una entrega fallida manualmente
curl -X POST "https://app.nodda.cl/api/webhooks/whe_a1b2c3d4-0000-4000-8000-aabbccddeeff/deliveries/del_a0b1c2d3-e4f5-4000-8000-aabbccddeeff/resend" \
  -H "Authorization: Bearer nodda_sk_tu_token"

El endpoint de entregas devuelve el historial de intentos (estado, código HTTP, timestamp) para cada evento.

On this page