Usage Events
A usage event is a single measurement: at this time, this customer used this agent to do this much of this thing. Logging usage events is how MarginFront learns what to bill for. Every time your agent does work, you log an event. At the end of the billing period, MarginFront rolls them up, applies the customer’s pricing plan, and generates an invoice. This is the most important endpoint in the whole API. Everything else (agents, signals, plans, subscriptions) is setup. This is the ongoing, every-day traffic.You don’t need to set anything up first. When you fire an event with a newcustomerExternalId,agentCode, orsignalName, MarginFront creates the customer, agent, or signal automatically. Your existing user IDs from your own database flow straight through. The dashboard updates the moment the event lands.
The endpoint
X-API-Key header (secret key required — mf_sk_*).
Batch: Even for a single event, the body wraps records in an array. You can send 1-100 records per request.
Fields per record
Each record uses one of two shapes:- Single-service shape: one event, one underlying service. The 90% case. Required fields: top-level
customerExternalId,agentCode,signalName,model,modelProvider(plus volume). - Multi-service shape: one event, multiple underlying services contributing to one business outcome (e.g., one report that called Claude AND queried Google Maps). Required fields:
customerExternalId,agentCode,signalName,services[]. Top-levelmodel/modelProvider/volume are omitted; each service entry carries its own.
services[], never both, never neither. Mixing shapes returns a 400 Bad Request with a clear English message.
Always required (both shapes)
Required for single-service shape (or use services[] instead)
Required for multi-service shape (or use single-service fields instead)
Each entry in
services[] accepts:
Optional fields
Tagging events for cost classification
Finance teams often need to split AI spend into Cost of Goods Sold (what it costs to serve paying customers) and Research & Development (internal experiments and test runs). To do that, add anenvironment to each record:
environment, MarginFront sorts that signal into a cost category for you:
This automatic sorting only happens while the signal is still unclassified. Once you set a category by hand in the dashboard, your choice sticks — later events still record their
environment, but they no longer change the category.
environment is optional. Leave it off and your events flow through exactly as before. You can see the breakdown on your dashboard’s Cost Management page.
Recording each event exactly once
Sometimes the same event gets sent twice. Your code retries after a timeout, a worker restarts and replays its queue, or a network blip leaves you unsure the first request landed. Without protection, each resend creates a second event — and your customer’s bill reads too high. To prevent that, give the event anidempotencyKey: a short, stable ID you pick that stays the same across retries of that one event. Send it on the record and MarginFront counts the event once, no matter how many times the request arrives.
- The first time MarginFront sees the key: the event is recorded normally.
- Any later request with the same key: it’s treated as a replay. No second event is created, and the response returns the original event — same
eventId, sametotalCostUsd— so a retry looks identical to the call that already worked.
- Pick a key that’s the same for the event but different across events. A good key is something you can recompute for the same piece of work: an order ID, a message ID, or a hash of the request. Two different events must not share a key, or the second one is dropped as a duplicate.
- Keys belong to your account. The same key value used by a different account never collides, so you don’t have to coordinate keys across customers.
- Retrying after a failure still works. Dedup only kicks in once an event has actually been recorded. If your first request failed before the event was saved, sending it again with the same key records it — you won’t lose an event just because you reused the key.
- It’s optional. Leave
idempotencyKeyoff and every request is recorded on its own — the same behavior as before. A missing key never deduplicates. - Max length is 255 characters.
LLM vs non-LLM events vs multi-service events
All three patterns use the same endpoint. The difference is which fields carry the “what happened” information. Single-service LLM event (OpenAI, Anthropic, Google, etc.) — cost is based on tokens:services[] becomes one cost line under it. The dashboard shows ONE event in the live feed; the “Cost by service” chart on the Cost tab shows where the rolled-up total split. The Gemini entry carries inputTokens, outputTokens, AND quantity: 1 because LLM services[] entries can track all three at once: tokens for cost, quantity for call-count analytics.
You can mix all three shapes in a single batch.
Example curl calls
Single LLM event (OpenAI):Response format (200 OK)
The endpoint always returns 200 OK — even if some records failed. You must check the response body to know what actually happened.
Success entry fields (single-service)
Success entry fields (multi-service)
The legacy top-level
model / modelProvider / inputTokens / outputTokens fields are NOT present on multi-service success entries (they live inside each services[] entry instead).
Failed entry fields
Error codes explained
What happens when the model isn’t recognized
MarginFront never drops events. If you send amodel + modelProvider combination that isn’t in the pricing table:
- The event is stored in
signal_eventswithusageCost: null(not zero — null preserves the ambiguity for backfill). - The response includes the event in
results.failed[]withcode: "NEEDS_COST_BACKFILL"andstored: true. - The “Needs attention” tile on the dashboard
/metrics-eventspage shows a count of these events. - Click through to see the events grouped by model+provider, with context (which agent, customer, signal sent them).
- Pick a known model from the dropdown, click “Map & backfill” — MarginFront creates a permanent mapping and retroactively calculates cost for every affected event.
- Future events with that model+provider auto-resolve — no manual step needed again.
stored: true. They’re already in the system. Retrying would create duplicates.
Mapping unknown models (API endpoints)
These endpoints power the dashboard drill-down page. You can also call them directly.List unknown-cost event groups
Response:
Map an unknown model to a known one
Listing events
Query your recorded usage events. Supports filtering and pagination. Use this when you need to see individual events — per-event drill-downs, audit trails, debugging a specific customer’s bill, or feeding a BI tool.x-api-key header. Either a secret key (mf_sk_*) or a publishable key (mf_pk_*) works — this is a read-only endpoint.
Query parameters
All optional. Without any, you get the most recent 20 events for your org.Example
Response (200 OK)
Understanding the event payload
Most fields are self-explanatory. A few are easy to trip over:-
usageCostis a string, not a number (e.g."0.00225"). We use strings to preserve decimal precision — prices can have many significant digits and JSON numbers would round. Convert withparseFloat()orNumber()before doing math. -
usageCostDatais the itemized cost breakdown — this is where per-model and per-dimension details live. Each key is"<model>/<dimension>"(e.g."gpt-4o/input","gpt-4o/output"). Each value has:cost— dollar amount for this line item (number)units— tokens for LLMs, or whatever quantity dimension was billed (number)costPerUnit— the rate applied (number)
usageCostequals the sum of everycostinsideusageCostData. If you need to ask “how many input tokens did this event use?”, readusageCostData["<model>/input"].units. If the event used multiple models or mixed LLM+non-LLM services, there will be multiple keys. -
quantityis a string (same precision reason asusageCost). -
signalis the nested signal object (id, name, shortName). Handy for display without a second lookup. -
subscriptionIdisnullwhen the customer had no active subscription at the time of the event.
Event processing states
TheeventProcessed field tells you where an event is in its lifecycle:
When
eventProcessed is "NEEDS_COST_BACKFILL", usageCost is null and usageCostData is empty. See Mapping unknown models above for the resolution flow.
Auto-provisioning
If you log a usage event for acustomerExternalId or agentCode that MarginFront has never seen, it will auto-create a minimal customer and/or agent on the spot. Convenient for prototyping — but means you won’t get an error for typos. Double-check in the dashboard if things “work” but show up with a name you don’t recognize.
Common HTTP errors
Using the Node SDK
The@marginfront/sdk package wraps this endpoint. See the SDK README for full documentation. Quick example:
fireAndForget: true setting, usage.record() never throws — network errors retry automatically via a local buffer. See the SDK docs for details.

