Inovacc Desarrolladores

Guías

Webhooks

Un webhook es la forma en que Eventos entrega a un servidor que tú operas: cada evento que coincide con una suscripción llega a tu endpoint HTTPS como un POST firmado. Esta guía cubre cómo crear una suscripción de webhook, la solicitud que recibe tu endpoint, la verificación de su firma, cómo funcionan los reintentos y los mensajes muertos, y cómo rotar el secreto de firma sin perder una entrega.

Qué es una suscripción de webhook

Una suscripción indica qué eventos quiere un receptor y cómo le llegan. Una suscripción de webhook tiene una lista de patrones de tipo y un bloque de entrega con "mode": "webhook" y tu url. A partir de ahí, cada evento publicado en el espacio de trabajo cuyo type coincida con uno de los patrones se envía con POST a esa URL, una vez por suscripción.

La entrega es al menos una vez. En operación normal cada evento llega una vez; un reintento, una respuesta lenta o una conexión perdida pueden hacer que llegue de nuevo. Por lo tanto, tu receptor debe construirse para aceptar un duplicado sin consecuencias, usando como clave el id del evento.

La URL debe ser https, en el puerto 443, sin nombre de usuario ni contraseña, sin fragmento, y con un host público: los nombres privados, de loopback, de enlace local y de uso local se rechazan cuando se crea la suscripción y se verifican de nuevo en cada entrega. Las redirecciones nunca se siguen.

Crea una

Llama a POST https://events.inovacc.dev/v1/workspaces/{workspace}/subscriptions con una clave secreta que tenga el permiso de escritura de suscripciones:

Lenguaje

⋮
    JSONEjemplo

    JSON

    {"types": ["order.*"], "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc"}}

    La respuesta es 201:

    Lenguaje

    ⋮
      JSONEjemplo

      JSON

      {"subscription_id": "sub_...", "types": ["order.*"],
       "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc", "secret_ref": "managed"},
       "created_at": "2026-10-06T12:00:00.000Z",
       "signing_secret": "whsec_..."}

      signing_secret es la clave que tu endpoint usa para verificar las entregas. Se muestra una sola vez, en esta respuesta, y nunca más: los listados muestran "secret_ref": "managed" y nada más. Guárdalo de inmediato en tu administrador de secretos. Si se pierde, rótalo (más abajo). Un espacio de trabajo contiene como máximo 50 suscripciones, cada una con 1 a 20 patrones.

      La solicitud de entrega

      Cada entrega es:

      POST https://hooks.example.com/inovacc
      content-type: application/json
      x-event-id: evt_01K...
      x-event-type: order.paid
      x-event-timestamp: 1791201600
      x-event-signature: v1=<64 lowercase hex characters>
      
      {"event_id":"evt_01K...","type":"order.paid", ... }
      

      x-event-timestamp es la hora Unix en segundos a la que se firmó la entrega. Durante una rotación de secreto, x-event-signature lleva una entrada por cada secreto vigente, la más nueva primero, separadas por comas: v1=<new>,v1=<old>.

      Responde con cualquier estado 2xx para confirmar la entrega. Cualquier otra cosa (un 4xx, un 5xx, ninguna respuesta en 10 segundos, un error de red) es un intento fallido y se reintentará.

      El envoltorio del evento

      El cuerpo es el envoltorio del evento, exactamente como lo reciben los suscriptores de todos los modos:

      CampoSignificado
      event_idevt_ seguido de 26 caracteres, asignado por Inovacc cuando se publicó el evento. Único por evento.
      typeEl tipo del evento, como order.paid: palabras en minúscula separadas por puntos, hasta 128 bytes.
      sourceLa aplicación que publicó el evento, tomada de su credencial.
      timeCuándo se publicó, RFC 3339 UTC con milisegundos.
      tenantLa organización, la cuenta, el espacio de trabajo y, cuando se conoce, la aplicación a la que pertenece el evento.
      idempotency_keyLa clave del publicador; cuando no envió ninguna, el event_id.
      dataLa carga JSON del publicador: lo que ocurrió, normalmente ids en lugar de objetos completos.

      Lee solo los campos que necesites, e ignora los campos que no conozcas.

      Verifica la firma

      Verifica cada entrega antes de analizarla o actuar sobre ella:

      1. Lee x-event-timestamp y los bytes del cuerpo sin procesar, exactamente como se recibieron, antes de cualquier análisis de JSON.
      2. Calcula HMAC-SHA256(secret, timestamp + "." + body), donde secret es la cadena whsec_... completa en UTF-8, y escríbelo como hexadecimal en minúscula.
      3. Divide x-event-signature por las comas y acepta la entrega cuando cualquier entrada v1= sea igual a tu valor, comparando en tiempo constante.
      4. Rechaza una marca de tiempo a más de cinco minutos de tu reloj, para que no se pueda repetir contra ti una entrega antigua.

      Ejemplo resuelto. Con el secreto example-key-0001, la marca de tiempo 1791201600 y el cuerpo {"hello":"world"}, el texto firmado es 1791201600.{"hello":"world"} y la firma son los 64 caracteres hexadecimales de abajo, impresos aquí en dos mitades que unes sin espacio:

      05080ab29a6abfb44033966c7837c2a1
      889fbf1a909f9cbd12b9dd5d12bb2bf5
      

      por lo que el encabezado dice v1= seguido de ambas mitades. Ejecuta tu verificador con este ejemplo antes de desplegarlo.

      TypeScript (Node.js 18 o posterior):

      Lenguaje

      ⋮
        TypeScriptEjemplo

        TypeScript

        import { createHmac, timingSafeEqual } from "node:crypto";
        
        export function verifyInovaccWebhook(
          secret: string,
          timestamp: string,
          signatureHeader: string,
          rawBody: Buffer,
          nowSeconds: number = Math.floor(Date.now() / 1000),
        ): boolean {
          const ts = Number(timestamp);
          if (!Number.isInteger(ts) || Math.abs(nowSeconds - ts) > 300) return false;
          const expected = createHmac("sha256", secret)
            .update(`${timestamp}.`)
            .update(rawBody)
            .digest();
          return signatureHeader.split(",").some((entry) => {
            const part = entry.trim();
            if (!part.startsWith("v1=")) return false;
            const given = Buffer.from(part.slice(3), "hex");
            return given.length === expected.length && timingSafeEqual(given, expected);
          });
        }

        Python (3.10 o posterior):

        Lenguaje

        ⋮
          PythonEjemplo

          Python

          import hashlib
          import hmac
          import time
          
          
          def verify_inovacc_webhook(secret: str, timestamp: str, signature_header: str,
                                     raw_body: bytes, now: int | None = None) -> bool:
              try:
                  ts = int(timestamp)
              except ValueError:
                  return False
              now = int(time.time()) if now is None else now
              if abs(now - ts) > 300:
                  return False
              expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
                                  hashlib.sha256).hexdigest()
              return any(
                  part.strip().startswith("v1=")
                  and hmac.compare_digest(part.strip()[3:], expected)
                  for part in signature_header.split(",")
              )

          Ambas funciones reciben el cuerpo sin procesar: configura tu framework web para que te entregue los bytes, no un objeto analizado, en esta ruta.

          Reintentos y tiempos de espera

          Tu endpoint tiene 10 segundos para responder. Un intento fallido se reintenta después de 10 segundos, y la espera se duplica en cada fallo adicional hasta 10 minutos. Cuando se alcanza el tope de 5 reintentos, el evento pasa a los mensajes muertos de tu espacio de trabajo. Un fallo afecta solo a la entrega que falló: los demás eventos, y las otras suscripciones del mismo evento, siguen fluyendo.

          Como una entrega puede repetirse, un 2xx que llega después de que tu endpoint ya procesó el evento es inofensivo siempre que deduplices por x-event-id.

          Rota el secreto

          POST /v1/workspaces/{workspace}/subscriptions/{id}/rotate-secret devuelve un secreto nuevo, una sola vez:

          Lenguaje

          ⋮
            JSONEjemplo

            JSON

            {"subscription_id": "sub_...", "signing_secret": "whsec_...", "previous_valid_until": "2026-10-07T12:00:00.000Z"}

            Hasta previous_valid_until (24 horas por defecto), ambos secretos firman: cada entrega lleva v1=<new>,v1=<old>. Despliega el secreto nuevo en tu receptor dentro de esa ventana; como el verificador acepta cualquier entrada que coincida, no se pierde nada mientras haces el cambio. Rotar de nuevo dentro de la ventana conserva todos los secretos que no han vencido. La rotación se aplica solo a las suscripciones de webhook (de lo contrario 409 wrong_delivery_mode).

            Mensajes muertos y reproducción

            GET /v1/workspaces/{workspace}/dead?limit=50 lista los mensajes muertos, los más recientes primero, con el evento, attempts, dead_at, replay_count y last_error:

            last_errorSignificado
            webhook_status_<code>Tu endpoint respondió ese estado, por ejemplo webhook_status_500.
            webhook_timeoutSin respuesta en 10 segundos.
            webhook_networkFalló la conexión.
            url_destination_blockedLa URL ahora apunta a un lugar al que las entregas no pueden ir.
            secret_missingNo se pudo usar el secreto de firma de la suscripción.

            Corrige la causa y luego POST /v1/workspaces/{workspace}/dead/{event_id}/replay. La respuesta es 202, y el mismo envoltorio, con el mismo event_id, se entrega de nuevo a las suscripciones que nunca lo recibieron. Los mensajes muertos se conservan 30 días.

            Buenas prácticas

            • Verifica antes de analizar: calcula la firma sobre los bytes sin procesar y luego analiza.
            • Haz el receptor idempotente: almacena cada x-event-id procesado y responde 2xx de inmediato ante uno que ya hayas visto.
            • Responde rápido: confirma con un 2xx y haz el trabajo lento después, desde tu propio trabajo.
            • Rechaza las marcas de tiempo caducadas (con más de cinco minutos de diferencia) y mantén sincronizado el reloj de tu servidor.
            • Conserva el secreto en un administrador de secretos y rótalo cuando se vaya alguien que lo conocía, o según un calendario.
            • Monitorea los mensajes muertos y dlq_inflow en GET /stats; reprodúcelos cuando el endpoint esté sano.
            • Devuelve 2xx solo cuando tengas el evento a salvo: un 2xx termina los reintentos.

            Ver también

            Actualizado el 2026-10-10.