Inovacc Desarrolladores

Referencia de la API

Eventos

Publica eventos y entrégalos a los suscriptores por webhook, consulta o transmisión en vivo, con mensajes muertos y reenvío.

URL base https://events.inovacc.dev

Descripción general

Eventos permite que una parte de tu sistema anuncie que algo ocurrió, y que cada parte interesada se entere, sin que las dos se conozcan. Un publicador envía un evento como order.paid a https://events.inovacc.dev; cada suscripción cuyo patrón coincida con el tipo del evento lo recibe, por webhook a tu endpoint HTTPS, por pull desde tu worker, o por transmisión en vivo mediante un WebSocket. Los eventos que no se pueden entregar se conservan como mensajes muertos, que puedes inspeccionar y reproducir.

El problema que resuelve es la difusión confiable. Llamar directamente a cada servicio interesado los acopla, pierde mensajes cuando uno está caído y reintenta mal. Eventos acepta la publicación una vez, la entrega al menos una vez a cada suscripción, reintenta las entregas fallidas con espera creciente, deduplica las publicaciones repetidas y conserva lo que no pudo entregar.

Usa Eventos para reaccionar a los cambios: enviar un correo cuando se paga un pedido, refrescar una caché cuando cambia un registro, enviar una notificación a una página. Mantén el estado en sí en Datos y los bytes en Archivos: un evento debe decir qué ocurrió, no cargar el objeto. Para la historia completa del receptor de webhooks, consulta la guía de webhooks.

Conceptos

Espacio de trabajo. Todas las rutas están bajo /v1/workspaces/{workspace}. El espacio de trabajo en la ruta es una afirmación que se verifica contra tu credencial: una clave de aplicación puede nombrar solo su propio espacio de trabajo, y cualquier otro responde 403 forbidden, igual que uno que no existe.

Eventos y el envoltorio. Publicas type (palabras en minúscula separadas por puntos, como invoice.created, hasta 128 bytes), data (cualquier valor JSON) y un idempotency_key opcional. Los suscriptores reciben un envoltorio con event_id (evt_ y 26 caracteres, asignado por Inovacc), type, source (tu aplicación, establecida a partir de la credencial), time, tenant, idempotency_key y data.

Niveles. Una publicación puede nombrar un tier: basic (el predeterminado), reliable o advanced. El nivel fija el mayor data que puede llevar un evento: 16 KiB para basic y reliable, 32 KiB para advanced.

Suscripciones y patrones. Una suscripción lista de 1 a 20 patrones en types, cada uno * (todo), un tipo exacto, o un prefijo que termina en .* (order.* coincide con order.paid y order.item.added), y un modo de entrega.

Modos de entrega. webhook envía con POST cada evento a tu URL HTTPS, firmado con el secreto de la suscripción. pull mantiene un buzón que tu código lee y confirma. websocket mantiene el mismo buzón y además lo transmite en vivo; pull sigue funcionando junto a la transmisión.

Al menos una vez. Cada evento llega una vez a cada suscripción que coincide en operación normal, pero un reintento o una confirmación perdida puede entregarlo de nuevo. Los receptores deduplican por event_id.

Mensajes muertos. Una entrega de webhook que sigue fallando se mueve a los mensajes muertos de tu espacio de trabajo, con el último error y el número de intentos, y se conserva 30 días.

Cómo funciona

Toda llamada lleva Authorization: Bearer <credential>. La credencial es el tenant: la organización, la cuenta y la aplicación se obtienen de ella, y nada en un cuerpo puede nombrar a otra. Una clave secreta es para servidores y puede usar todas las rutas. Una clave publicable (apb_...) solo puede abrir una transmisión, desde un origen que su aplicación liste. El token de un usuario final no puede usar nada. Cada ruta necesita su propio permiso, como events:event.publish, events:subscription.write o events:dead.replay.

Publicar. POST /events con {"event": {...}} o {"events": [...]} (de 1 a 100). La respuesta es 202 con accepted (cada uno con su event_id y duplicate) y rejected (cada uno con un índice y un código); un evento incorrecto nunca bloquea a los demás. Enviar un idempotency_key ya usado en el espacio de trabajo en las últimas 168 horas devuelve el event_id original con duplicate: true y no entrega nada nuevo.

Entrega por webhook. Inovacc envía con POST el envoltorio a tu URL con x-event-id, x-event-type, x-event-timestamp y x-event-signature: v1=<hex>, un HMAC-SHA256 de la marca de tiempo y del cuerpo sin procesar. Cualquier 2xx dentro de 10 segundos es una entrega; cualquier otra cosa se reintenta después de 10 segundos, y la espera se duplica hasta 10 minutos, hasta que se alcanza el tope de 5 reintentos y el evento se convierte en un mensaje muerto. Las redirecciones nunca se siguen, y tu URL debe ser HTTPS pública en el puerto 443.

Pull. POST /subscriptions/{id}/pull con max (de 1 a 100, por defecto 10) y lease_seconds (de 5 a 300, por defecto 30) devuelve los mensajes más antiguos, cada uno con su delivery_count. Un mensaje obtenido queda oculto durante su arrendamiento; confírmalo con POST .../ack y los ids, o regresa cuando termina el arrendamiento.

Transmisión. GET /subscriptions/{id}/stream se actualiza a un WebSocket con el subprotocolo inovacc.v1; un navegador pasa su clave como un segundo subprotocolo, bearer.<key>. Cada trama es un envoltorio; responde {"ack": [...]} para confirmar. Una trama sin confirmar se vuelve a enviar después de su arrendamiento.

Monitoreo. GET /stats devuelve published, completed, depth, oldest_pending_age_seconds, retries, retry_rate y dlq_inflow para el espacio de trabajo. Cada llamada se registra en el Registro de actividad. Lo que se mide es el evento entregado.

Primeros pasos

Necesitas una clave secreta vinculada a tu espacio de trabajo con los permisos de eventos (Autenticación).

  1. Crea una suscripción pull. POST /v1/workspaces/{workspace}/subscriptions con {"types":["demo.*"],"delivery":{"mode":"pull"}} (consulta los ejemplos). La respuesta es 201 con un subscription_id.
  2. Publica. POST .../events con {"event":{"type":"demo.hello","data":{"n":1}}}. La respuesta es 202 con una entrada en accepted y su event_id.
  3. Obtén con pull. POST .../subscriptions/{id}/pull con {}. Tu evento está en messages, con delivery_count 1.
  4. Confirma. POST .../subscriptions/{id}/ack con {"ids":["<event_id>"]} responde {"acked":1}; un segundo pull viene vacío.
  5. Agrega un webhook. Crea una segunda suscripción con {"mode":"webhook","url":"https://<your endpoint>"}, conserva el signing_secret que la respuesta muestra una sola vez, y sigue la guía de webhooks para verificar las entregas.

Casos de uso

Cumplimiento de pedidos. El pago publica order.paid con el id del pedido en data y el id del pago como idempotency_key. Una suscripción por webhook para order.* llega al sistema del almacén; una suscripción pull alimenta el trabajo de facturación. Un pago reintentado por el cliente se publica una sola vez.

Actualizaciones en vivo en una página. Un panel abre una transmisión en una suscripción websocket para ticket.* con una clave publicable desde su propio origen, muestra cada ticket nuevo a medida que llega la trama, y la confirma.

Recuperación tras una caída. El endpoint de un socio estuvo caído una hora. Sus entregas terminaron en mensajes muertos con last_error webhook_timeout. Cuando vuelve, tu equipo lista GET /dead y reproduce cada evento; va solo a las suscripciones que nunca lo recibieron.

Límites y precios

LímiteValor
Eventos por publicación100
Cuerpo de la solicitud5 MiB
data por evento16 KiB (basic, reliable), 32 KiB (advanced)
Ventana de deduplicación de idempotency_key168 horas
Suscripciones por espacio de trabajo; patrones por suscripción50; 20
Mensajes por pull; arrendamiento100; de 5 a 300 segundos
Tiempo de respuesta del webhook10 segundos
Reintentosdesde 10 segundos, duplicándose hasta 10 minutos; tope de 5, luego mensaje muerto
Mensajes muertos conservados30 días
Trama de transmisión desde el cliente49,152 bytes
Tasa600 llamadas por minuto por credencial, por ubicación

Precios: a consultar. La unidad de precio es el evento entregado.

Errores

EstadoCódigoQué significa y qué hacer
400invalid_bodyEl cuerpo está mal formado, o nombra source (proviene de tu credencial).
400invalid_querySolo GET /dead acepta una consulta (limit).
400invalid_subscription, invalid_delivery, invalid_limit, invalid_idCorrige la suscripción, la entrega o el parámetro.
400invalid_url, url_not_https, url_has_credentials, url_port_not_allowed, url_destination_blockedLa URL del webhook debe ser HTTPS pública en el puerto 443, sin credenciales.
401invalid_credentialsEnvía una credencial válida.
403forbidden, account_requiredFalta un permiso o el espacio de trabajo es incorrecto; usa una clave de aplicación.
403publishable_key_not_allowed, origin_not_allowed, secret_key_in_browserUna clave publicable solo puede transmitir, desde un origen listado; mantén las claves secretas en servidores.
404subscription_not_found, dead_event_not_foundNo existe en tu espacio de trabajo.
409wrong_delivery_mode, too_many_subscriptions, secret_not_managedLa operación no encaja con el modo de entrega, o estás en el tope de 50 suscripciones.
413payload_too_largeEl cuerpo supera 5 MiB.
426upgrade_requiredLa ruta de transmisión necesita una actualización a WebSocket.
429rate_limited, too_many_streamsReduce el ritmo; cierra los sockets que no necesites.
503service_unavailable, events_disabled, signing_unavailableReintenta más tarde; una publicación reintentada con las mismas claves reutiliza sus ids.

Un evento rechazado dentro de un 202 lleva su propio código: invalid_event, unknown_field, invalid_type, invalid_idempotency_key, payload_too_large o data_required.

Buenas prácticas

  • Envía un idempotency_key derivado de lo que ocurrió (el id del pago, la versión del registro), para que una publicación reintentada no sea un segundo evento.
  • Deduplica por event_id en cada receptor: la entrega es al menos una vez.
  • Pon ids en data, no objetos: obtén el estado actual desde Datos cuando manejes el evento.
  • Responde los webhooks rápido con un 2xx y haz el trabajo después; 10 segundos es el límite.
  • Verifica la firma de cada webhook antes de analizar el cuerpo (guía de webhooks).
  • Confirma los mensajes obtenidos con pull después de procesarlos, y elige un arrendamiento más largo que tu tiempo de procesamiento.
  • Vigila depth y dlq_inflow en /stats, y reproduce los mensajes muertos una vez corregida la causa.

Autenticación

Toda llamada lleva tu clave; tu organización viene de ella. Consulta la guía de autenticación.

EncabezadoAuthorizationBearer <clave de API>

Endpoints

POST /v1/workspaces/{workspace_id}/events

GET /v1/workspaces/{workspace_id}/subscriptions

POST /v1/workspaces/{workspace_id}/subscriptions

DELETE /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}

POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/rotate-secret

POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/pull

POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/ack

GET /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/stream

GET /v1/workspaces/{workspace_id}/dead

POST /v1/workspaces/{workspace_id}/dead/{event_id}/replay

GET /v1/workspaces/{workspace_id}/stats

Ejemplo

Lenguaje

⋮
POST /v1/workspaces/{workspace_id}/eventsEjemplo

cURL

curl -X POST "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events" \
  -H "Authorization: Bearer $INOVACC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
}'

TypeScript

const body: Record<string, unknown> = {
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
};

const url = "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events";

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(body),
});

console.log(response.status, await response.text());

Python

import json
import os
import urllib.request

body = {
    "event": {
        "type": "order.paid",
        "idempotency_key": "pay_123",
        "data": {
            "order_id": "ord_42",
        },
    },
}

request = urllib.request.Request(
    "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events",
    data=json.dumps(body).encode(),
    method="POST",
    headers={
        "User-Agent": "inovacc-python-sample",
        "Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
        "Content-Type": "application/json",
    },
)

with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Go

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

const url = "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events"

const body = `{
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
}`

func main() {
	req, err := http.NewRequest("POST", url, strings.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	out, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(res.Status, string(out))
}

Rust

// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// Cargo.toml: serde_json = "1"

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("INOVACC_API_KEY")?;
    let body = serde_json::json!({
        "event": {
            "type": "order.paid",
            "idempotency_key": "pay_123",
            "data": {
                "order_id": "ord_42"
            }
        }
    });
    let response = reqwest::blocking::Client::new()
        .post("https://events.inovacc.dev/v1/workspaces/{workspace_id}/events")
        .bearer_auth(api_key)
        .json(&body)
        .send()?;
    println!("{} {}", response.status(), response.text()?);
    Ok(())
}

JavaScript

const body = {
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
};

const url = "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events";

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(body),
});

console.log(response.status, await response.text());

PHP

<?php

$body = <<<'JSON'
{
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
}
JSON;

$curl = curl_init('https://events.inovacc.dev/v1/workspaces/{workspace_id}/events');
curl_setopt_array($curl, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);

$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);

Ruby

require "net/http"
require "uri"

uri = URI('https://events.inovacc.dev/v1/workspaces/{workspace_id}/events')
body = <<~'JSON'
{
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
}
JSON

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = body

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end

puts "#{response.code} #{response.body}"

Java

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("INOVACC_API_KEY");
        String body = "{" +
                "\"event\": {" +
                "\"type\": \"order.paid\"," +
                "\"idempotency_key\": \"pay_123\"," +
                "\"data\": {" +
                "\"order_id\": \"ord_42\"" +
                "}" +
                "}" +
                "}";

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://events.inovacc.dev/v1/workspaces/{workspace_id}/events"))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .method("POST", HttpRequest.BodyPublishers.ofString(body))
                .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}

C#

using System.Text;

var body = "{" +
    "\"event\": {" +
    "\"type\": \"order.paid\"," +
    "\"idempotency_key\": \"pay_123\"," +
    "\"data\": {" +
    "\"order_id\": \"ord_42\"" +
    "}" +
    "}" +
    "}";

var url = "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");

using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");

Kotlin

import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun main() {
    val apiKey = System.getenv("INOVACC_API_KEY")
    val body = "{" +
        "\"event\": {" +
        "\"type\": \"order.paid\"," +
        "\"idempotency_key\": \"pay_123\"," +
        "\"data\": {" +
        "\"order_id\": \"ord_42\"" +
        "}" +
        "}" +
        "}"

    val request = HttpRequest.newBuilder()
        .uri(URI.create("https://events.inovacc.dev/v1/workspaces/{workspace_id}/events"))
        .header("Authorization", "Bearer " + apiKey)
        .header("Content-Type", "application/json")
        .method("POST", HttpRequest.BodyPublishers.ofString(body))
        .build()

    val response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString())
    println("${response.statusCode()} ${response.body()}")
}

Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let body = #"""
{
  "event": {
    "type": "order.paid",
    "idempotency_key": "pay_123",
    "data": {
      "order_id": "ord_42"
    }
  }
}
"""#

let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""

let url = URL(string: "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(body.utf8)

let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))
Detalles de la fuente

Entrada del catálogo

Id
identity/component-taxonomy/capabilities#events
Repositorio
identity
Ruta
contracts/component-taxonomy/catalog/capabilities.json
Commit
08ef3cd75d82

Ejemplo

Id
identity/component-taxonomy/quickstart#events
Repositorio
identity
Ruta
contracts/component-taxonomy/catalog/capabilities.json
Commit
08ef3cd75d82
Actualizado el 2026-10-10.