Receive OpenRouter Broadcast traces as usage rows
OpenRouter's spend is invisible to every Hanzo money lens because those lenses read hanzo.cloud_usage and OpenRouter meters keys of its own.
POST /v1/integrations/openrouter/webhook
| Address | https://api.hanzo.ai/v1/integrations/openrouter/webhook |
| Method | POST |
| Operation | post_integrations_openrouter_webhook |
| Auth | Authorization: Bearer $HANZO_API_KEY |
OpenRouter's spend is invisible to every Hanzo money lens because those lenses read hanzo.cloud_usage and OpenRouter meters keys of its own. Point a Broadcast destination (Settings ▸ Observability ▸ Webhook) at this door and each generation span becomes ONE row in that same ledger with provider openrouter, so one query answers what we spend everywhere. Enable the Cost and Identity field categories: cost is the money and identity carries openrouter.api_key_name, which is what says WHICH key spent it — it lands in account as openrouter/<key name>.
AUTHENTICATION IS A HANZO KEY. Broadcast signs nothing; its only authentication is the destination's Headers map, so send a key as Authorization: Bearer pk-… and it is admitted exactly as /v1/event admits a beacon's: a project key resolves through the project that minted it, an IAM-issued key through IAM. That key names the org every row is filed under; it can write and cannot read. No key, or a key that names no org, is 401 and nothing is stored.
The body is OTLP/JSON — {resourceSpans:[{scopeSpans:[{spans:[…]}]}]} — exactly as OpenTelemetry defines it; the model, tokens and cost are read from each span's gen_ai.* attributes and the key name from openrouter.api_key_name. The answer is {stored, dropped}: how many generations became rows, and how many spans named no model. Those are OpenRouter's trace and span parents — they carry no cost to meter. An empty payload stores nothing and answers 200, which is what makes Test Connection pass. A warehouse that cannot take the rows answers 503 so the delivery shows red and can be replayed: a row is keyed by its span id, so a redelivery collapses rather than double-counting.
Request
1 field, body application/json.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
* | body | any | — |
Response
| Status | Body | Meaning |
|---|---|---|
2XX | object | Success |
2XX body — 1 field.
| Field | In | Type | Always | Description |
|---|---|---|---|---|
* | body | any | — |
Failure carries the platform error shape — see Errors.
Examples
hanzo integrations openrouter webhookimport { Configuration, IntegrationsApi } from 'hanzoai';
const api = new IntegrationsApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postIntegrationsOpenrouterWebhook();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import IntegrationsApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = IntegrationsApi(client).post_integrations_openrouter_webhook()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.IntegrationsAPI.PostIntegrationsOpenrouterWebhook(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, integrations_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = integrations_api::post_integrations_openrouter_webhook(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.IntegrationsApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new IntegrationsApi(client).postIntegrationsOpenrouterWebhook();curl -X POST https://api.hanzo.ai/v1/integrations/openrouter/webhook \
-H "Authorization: Bearer $HANZO_API_KEY"The door reaches integrations through the integrations tool, which names its 45 operations with its own verbs — this one among them, under a name only the door declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "list_connectors"
}
}
}'How is this guide?