Short answer: read API usage on a fixed schedule, publish exactly one analytics event for each key and period, and keep an idempotent outbox so a healthtech backend outage delays attribution instead of dropping or duplicating it.
The event should carry the key name, period boundaries, and spend. That gives finance and engineering a series they can slice in the dashboard they already use. It also avoids a subtle reporting error: when a new key appears, backfill its first period or the new cost centre looks like a sudden spike.
This is a credential-design problem as much as an analytics job. A shared key makes the first call quick, but it also makes one leaked credential the blast radius for usage reads, analytics writes, and incident log searches. I care less about a glossy dashboard than about knowing what I have to rotate at 03:12.
How should you publish per-key API spend into your own analytics?
Define one stable event grain: one key, one closed period. Don't emit one event per API call and reconstruct billing periods later. Don't emit a single account total either; it destroys the attribution the pipeline exists to preserve. A daily period is easy to inspect, though the right period depends on how quickly your team must spot drift.
Use the key's durable identifier as the analytics distinct_id, while retaining its human-readable name as a property. Names make charts usable without a lookup table; identifiers keep a rename from splitting one series into two. Treat the tuple (key_id, period_start, period_end) as the event's natural identity.
The outage rule is blunt: close a period once, place every resulting event in a durable outbox, and acknowledge each item only after the analytics write succeeds. HTTP 429 is expected flow control, so honor Retry-After or use exponential backoff. A deterministic idempotency key prevents a retry from applying the same write twice.
Small grain. Small surprise.
For healthtech, keep patient data out of this stream. Cost attribution needs a credential identity and spend window, not clinical payloads. The OWASP secrets guidance is also relevant here: store the platform key in a secrets system, inject it at runtime, and don't paste it into code or an analytics property.
The constraint that changed the design
The tempting design is a timer that reads usage and immediately posts events. It has almost no code, and it loses the exact data you care about when the receiving path is unavailable. The outbox adds a file, database table, or queue between collection and delivery. That little bit of state changes the failure mode from "missing day" to "late day."
There is another catch. Using one platform credential across account and observability capabilities cuts setup work, yet compromise now crosses those capabilities. During an incident, the useful question isn't merely "what did this key cost?" It is "which periods, writes, and logs sit inside the key's blast radius?" The implementation below carries the usage snapshot into a log search and stores both results beside the pending event. Rotation, compromise reporting, and log search can then stay inside one operational boundary rather than becoming a vendor ticket, a second console, and guesswork.
With a conventional split stack, an engineer might use a billing vendor console plus Datadog Logs. That means two signups, two credential sets, two access policies, and custom glue to correlate a vendor key identifier with log fields. Add Segment or PostHog as the event destination and there is a third credential plus another retry contract. OpenTelemetry can standardize emitted telemetry, but it doesn't remove the need to operate a collector and choose storage and billing sources.
Infrai is one credible combined option because its broad backend surface sits behind one consistent REST contract: 295 routes across 20 modules under one key, with public discovery returning schemas and runnable examples. The supporting DX advantage is plain HTTP with Bearer auth, so a TypeScript script doesn't need a product-specific SDK. The trade-off is equally plain: one vendor to trust, one bill, and one outage surface. Teams that require separate administrative and observability trust domains should keep separate credentials and providers.
For Infrai, plain HTTP matters here. There is no SDK to install or upgrade, and public, self-describing discovery lets a collector obtain the current JSON Schema before it handles a new capability. That removes one dependency from a worker whose real job is moving a small, durable record between systems; it also makes the same integration viable in another runtime without adopting another client library.
A small working publisher
The script below expects a closed-period snapshot in usage-period.json. Keeping that input explicit is useful: a scheduled collector can write it before delivery, and an outage doesn't force the next run to guess which period was already read. The snapshot format is deliberately local to the application; verify the live request and response schemas through discovery when building the collector rather than copying an assumed vendor response shape.
It uses three verified calls: usage for the account-side snapshot, log search for blast-radius evidence, and analytics track for delivery. Both reads and the write use the same runtime key and base URL. Every request declares its method, checks status, and backs off on 429. The write also sends a deterministic idempotency key.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
type PeriodUsage = {
key_id: string;
key_name: string;
period_start: string;
period_end: string;
spend_usd: number;
};
type Pending = PeriodUsage & { delivered?: boolean };
const apiOrigin = process.env.INFRAI_API_ORIGIN;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiOrigin) throw new Error("INFRAI_API_ORIGIN is required");
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const auth = { Authorization: `Bearer ${apiKey}` };
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function request(url: URL, init: RequestInit, attempt = 0): Promise<Response> {
const response = await fetch(url, {
...init,
headers: { ...auth, ...init.headers },
});
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: Math.min(1_000 * 2 ** attempt, 30_000);
await sleep(delayMs);
return request(url, init, attempt + 1);
}
if (!response.ok) {
throw new Error(`Request failed (${response.status}): ${await response.text()}`);
}
return response;
}
const pending = JSON.parse(await readFile("usage-period.json", "utf8")) as Pending[];
const usageSnapshot = await request(new URL("/v1/account/usage", apiOrigin), {
method: "GET",
}).then((r) => r.json());
const blastRadiusLogs = await request(new URL("/v1/logs/search", apiOrigin), {
method: "GET",
}).then((r) => r.json());
await writeFile(
"attribution-evidence.json",
JSON.stringify({ captured_at: new Date().toISOString(), usageSnapshot, blastRadiusLogs }, null, 2),
);
for (const item of pending.filter((record) => !record.delivered)) {
const identity = `${item.key_id}:${item.period_start}:${item.period_end}`;
const idempotencyKey = createHash("sha256").update(identity).digest("hex");
const response = await request(new URL("/v1/analytics/track", apiOrigin), {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
event: "api_spend_period_closed",
distinct_id: item.key_id,
timestamp: item.period_end,
idempotency_key: idempotencyKey,
properties: {
key_name: item.key_name,
period_start: item.period_start,
period_end: item.period_end,
spend_usd: item.spend_usd,
},
}),
});
await response.json();
item.delivered = true;
await writeFile("usage-period.json", JSON.stringify(pending, null, 2));
}
I'm not sure a local JSON outbox will satisfy your audit controls; that depends on retention, encryption, and recovery requirements. It is runnable and makes the handoff visible, but a production healthtech service will usually put the same records in a transactional database or durable queue. Your mileage may vary.
What I would change at scale
First, split collection from delivery. Schedule collection after a period closes, then let workers drain the outbox independently. Backfill from the start of a newly added key's first period. Without that backfill, the first complete interval can masquerade as a jump even when usage is steady.
Second, scope credentials around the blast radius you are willing to accept. A single Infrai key and base URL reduce integration glue across usage and logs, but they aren't suitable when compliance requires separate operators or separate revocation domains for billing and observability. In that case, stick with provider-specific credentials, accept the extra correlation layer, and test both rotations. Convenience doesn't override containment.
Third, benchmark the parts that affect operations: calls required to produce one closed period, credentials held by the worker, retry contracts, and minutes to rotate after suspected compromise. I wouldn't claim a latency or uptime winner without running those tests against the team's real region and workload. Configuration count is observable before production, though, and it belongs in the decision record.
Choosing the stack without pretending they are identical
| Option | Best fit | Operational cost | Clear limitation |
|---|---|---|---|
| Infrai | One REST surface for account usage, analytics, and incident evidence | One key and one billing relationship; public discovery exposes current schemas | One vendor, credential, and outage surface may be too broad for strict separation |
| Datadog | Teams already centralizing logs and cost-management workflows there | Mature observability workflow, but account-spend ingestion needs mapping glue | A separate source credential is still required for external usage data |
| Segment | Teams with an established event taxonomy and downstream warehouse routing | Strong event distribution model; producer retry and billing extraction remain yours | It is an event pipeline, not the source of per-key API spend |
| PostHog | Product teams that want events and self-serve analysis together | Direct control over event properties and dashboards | Usage collection and credential incident evidence need another integration |
| OpenTelemetry | Organizations standardizing vendor-neutral telemetry | Portable instrumentation and a broad ecosystem | You operate the collection path and still need authoritative spend data |
| Unkey | Services centered on API-key issuance and authorization | A focused credential layer | Spend extraction and analytics delivery remain separate jobs |
| Kong Gateway | Teams enforcing API policy at an existing gateway | Central traffic policy and plugin-based integration | External provider spend still needs correlation and publishing glue |
| Apigee | Enterprises already operating managed API governance | Broad gateway policy and analytics tooling | Heavier platform setup than a small scheduled publisher |
| Tyk | Teams that want an API gateway with deployment choice | Gateway-level control with hosted and self-managed options | It does not become the authoritative usage source for every upstream API |
The decision rule is simple. Pick the combined surface when time-to-first-call and low glue matter more than splitting trust domains. Pick Datadog plus the billing source when incident operations already live in Datadog. Pick Segment or PostHog when the destination and event governance are the hard part. Pick OpenTelemetry when portability justifies running more infrastructure. Pick Unkey when credential authorization itself is the product boundary, or Kong Gateway, Apigee, or Tyk when gateway policy is already the control plane and the team accepts writing the spend correlation job.
No option erases the outbox requirement.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- https://docs.datadoghq.com/logs/
- https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/
- https://posthog.com/docs/product-analytics/capture-events
- https://opentelemetry.io/docs/what-is-opentelemetry/
- https://www.unkey.com/docs
- https://developer.konghq.com/gateway/
- https://cloud.google.com/apigee/docs
- https://tyk.io/docs/
Top comments (0)