Core concepts
Emitters
Settled usage, delivered where you bill from.
An emitter is a destination for settled usage: an https endpoint you own, a Metronome ingest, or an Orb ingest. Every call that settles is delivered to each enabled emitter on the application, after the money has been recorded, so a destination that is down cannot change what a call cost or make the caller wait for it.
seams emitters add webhook --url https://example.com/seams/usage
# emitter emt_01K2VY9WQ4X00000000EMT
# kind webhook
# name https://example.com/seams/usage
# enabled true
# destination https://example.com/seams/usage
# events usage.recorded
# customer id end_user_id
# credential …9f2c
# signing secret whsec_…The signing secret is in that response and nowhere else. Store it before you run the next command.
Managing them
| Command | What it does |
|---|---|
seams emitters list | Every destination, where it sends and whether it is on |
seams emitters show <id> | One destination, its events and the last four of its credential |
seams emitters add webhook --url <https url> | Post batches to an endpoint you own |
seams emitters add metronome --api-token <token> --event-type <type> | Ingest into Metronome |
seams emitters add orb --api-key <key> --event-name <name> | Ingest into Orb |
seams emitters update <id> --disable | Stop delivering, keep the configuration |
seams emitters update <id> --api-token <token> | Rotate the stored credential |
seams emitters rm <id> | Stop delivering there, and forget the credential |
seams emitters test <id> | Deliver one sample now |
The same six operations are GET, POST, PATCH and DELETE on /v1/emitters, and the console has a screen for them. Add takes --name to label a destination and --disabled to configure one without turning it on.
One event
usage.recorded fires when a call settles and writes a usage row. It is the only event an emitter carries; any other name is refused when you configure it, rather than accepted and never delivered.
seams emitters add webhook --url https://example.com/usage --events usage.settled
# error
# code unknown_event
# message no emitter event 'usage.settled': usage.recorded
# fix seams emitters add webhook --events usage.recordedWhat a webhook receives
A batch is posted as JSON with two headers: seams-signature, and seams-idempotency-key naming the batch. The body carries the settled amounts as decimal strings of micro-dollars, so no JSON number can round a balance.
{
"id": "usgb_4b1f7c0a92e5f3d18a6c07b2e9d45183",
"type": "usage.recorded",
"events": [
{
"idempotencyKey": "usg_01JB2X8Q7M4K00000000SET",
"usageRecordId": "usg_01JB2X8Q7M4K00000000SET",
"organizationId": "org_01JQZ8N4KP0000000000ACME",
"endUserId": "eu_01JQZ8N4KP00000000000ANNA",
"apiKeyId": "key_01JQZ8N4KP000000000000KEY",
"requestId": "req_01JQZ8N4KP000000000000REQ",
"provider": "openai",
"model": "openai/gpt-4o-mini",
"streamed": false,
"tokens": {
"inputTokens": 1204,
"outputTokens": 318,
"cachedInputTokens": 0,
"reasoningTokens": 0,
"tokenCountSource": "provider_reported"
},
"providerCostMicros": "3044",
"billedAmountMicros": "3652"
}
]
}There is no prompt and no completion here, and there is nowhere for one to appear: the usage record has no body column to read from.
Verify before you parse
The signature header carries the second it was signed and an HMAC-SHA256 over t, a full stop, and the raw body, keyed with your signing secret. Sign the same string yourself and compare in constant time. A body you have parsed is a body you have already trusted.
seams-signature: t=1756060800,v1=5257a869e7ecebeda32affa62cdca3fa…import { createHmac, timingSafeEqual } from "node:crypto"
const verify = (rawBody: string, header: string, secret: string): boolean => {
const parts = new URLSearchParams(header.replace(/,/g, "&"))
const t = parts.get("t") ?? ""
const v1 = parts.get("v1") ?? ""
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
return v1.length === expected.length &&
timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}Delivery
Delivery is at-least-once, so your handler has to be idempotent. Two keys make that cheap: the POST carries seams-idempotency-key for the whole batch, and every event inside carries an idempotencyKey equal to its usage record id. Store either one and a repeat is a no-op.
A batch that is not accepted is retried twice more, after 200ms and then 400ms. 408, 425, 429, 500, 502, 503 and 504 are treated as worth retrying; any other status is taken as an answer. The body is identical on every attempt, though each attempt is signed afresh, so t and v1 differ.
There is no queue behind those three attempts. A batch that fails all three is not delivered later, and the call it describes is still in seams usage, seams requests and GET /v1/usage. so reconcile from there, and answer 2xx on receipt rather than after your own downstream work.
Where a webhook may point
A webhook url must be https, and its host must resolve to a public address. Private ranges, loopback and link-local are refused, so a url cannot be used to reach inside our network. The address is checked on every delivery, not once when you save it, and a url that resolves somewhere private is a configuration fault rather than something to retry.
Metronome
Settled usage arrives as ingest events. transaction_id is the usage record id, which is what stops a repeat being rated twice, and event_type is the one you named when you added the emitter.
seams emitters add metronome \
--api-token mtr_… \
--event-type app.inference \
--customer-id organization_id[
{
"transaction_id": "usg_01JB2X8Q7M4K00000000SET",
"customer_id": "org_01JQZ8N4KP0000000000ACME",
"event_type": "app.inference",
"timestamp": "2026-09-07T09:20:00.000Z",
"properties": {
"provider": "openai",
"model": "openai/gpt-4o-mini",
"streamed": false,
"input_tokens": 1204,
"output_tokens": 318,
"cached_input_tokens": 0,
"reasoning_tokens": 0,
"provider_cost_micros": "3044",
"billed_amount_micros": "3652",
"request_id": "req_01JQZ8N4KP000000000000REQ",
"api_key_id": "key_01JQZ8N4KP000000000000KEY"
}
}
]Orb
The same events, and the same properties, in Orb's shape. idempotency_key is again the usage record id, and event_name is the one you named.
seams emitters add orb --api-key orb_… --event-name inference{
"events": [
{
"idempotency_key": "usg_01JB2X8Q7M4K00000000SET",
"external_customer_id": "eu_01JQZ8N4KP00000000000ANNA",
"event_name": "inference",
"timestamp": "2026-09-07T09:20:00.000Z",
"properties": { "provider": "openai", "billed_amount_micros": "3652" }
}
]
}The properties both vendors receive
| Property | What it holds |
|---|---|
provider | Which vendor served the call |
model | The model that answered, not the name that was asked for |
streamed | Whether the call was streamed |
input_tokens, output_tokens | Counted for the attempt that settled |
cached_input_tokens, reasoning_tokens | Zero when the provider reports neither |
provider_cost_micros | What the call cost to serve, as a decimal string |
billed_amount_micros | What it billed, as a decimal string |
request_id, api_key_id | Present when the call carried them |
Which customer the usage is filed under
--customer-id end_user_id is the default and sends the end user the key belongs to, falling back to the organization when a call has no end user. --customer-id organization_id files everything under the customer instead, which is what you want when you bill your account rather than their customers.
The id on the wire is ours. eu_…, not the external id you minted the key with. seams users show alice prints both, so map them once when you create the customer in Metronome or Orb.
Credentials
Every credential is sealed on the row with an envelope key, and only the delivery path opens it. Nothing else can read one back: list and show print the last four characters and the destination, never the secret.
A Metronome token or an Orb key is replaced in place with seams emitters update <id> --api-token … or --api-key …. A webhook's signing secret is not: it is issued once when the emitter is added, so rotating it means removing the emitter and adding it again.
Check it before real usage depends on it
test delivers one sample usage.recorded now, with an id of its own, so a destination that cannot be reached says so here rather than at your first settled call. The sample belongs to no end user, so it is filed under your organization whichever customer id an emitter is set to.
seams emitters test emt_01K2VY9WQ4X00000000EMT
# emitter emt_01K2VY9WQ4X00000000EMT
# kind webhook
# destination https://example.com/seams/usage
# event usage.recorded
# idempotency key usg_01K2VY9WQ4X0000SAMPLE
#
# sample
# usage record id usg_01K2VY9WQ4X0000SAMPLE
# customer id org_01JQZ8N4KP0000000000ACME
# request id req_01K2VY9WQ4X0000SAMPLE
# provider sample
# model sample
# streamed false
# input tokens 1820
# output tokens 310
# billed amount micros 4200
#
# delivered true
# reason
# latency ms 142
# sent at 2026-09-07T09:20:00.000Z