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.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
Required fields
| Field | Type | Description |
|---|---|---|
customerExternalId | string | Your customer’s ID in your system. The externalId you set when you created the customer (not the internal UUID). Example: "acme-001". |
agentCode | string | The agent/product code from the dashboard. Example: "cs-bot-v2". |
signalName | string | The metric being tracked. Usually matches the signal’s shortName. Example: "messages". |
model | string | The model identifier from your provider. Pass whatever your provider SDK returned: response.model from OpenAI/Anthropic, the service SKU for non-LLM tools. Case-insensitive, whitespace trimmed. Examples: "gpt-4o", "claude-sonnet-4-6", "twilio-sms", "textract-standard". |
modelProvider | string | The provider name, lowercase. This tells MarginFront which pricing table to look in. Required because different providers can have models with the same name — without this field, MarginFront can’t tell if "gpt-4o" means OpenAI’s or a fine-tune on another platform. Examples: "openai", "anthropic", "google", "twilio", "aws". |
Optional fields
| Field | Type | Default | Description |
|---|---|---|---|
inputTokens | integer (≥ 0) | — | Number of input (prompt) tokens. Required for LLM cost calculation. Ignored for non-LLM services. |
outputTokens | integer (≥ 0) | — | Number of output (completion) tokens. Required for LLM cost calculation. Ignored for non-LLM services. |
quantity | integer (≥ 0) | 1 | Number of billing units for non-LLM services (pages processed, SMS sent, API calls, etc.). |
usageDate | ISO 8601 string | now | When the usage actually happened. Use this for back-filling historical events. |
metadata | object | {} | Custom key-value pairs. Stored but not interpreted. |
LLM vs non-LLM events
Both patterns use the same endpoint. The difference is which fields carry the “how much” information. LLM event (OpenAI, Anthropic, Google, etc.) — cost is based on tokens: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
| Field | Description |
|---|---|
customerExternalId | Echoed from your request |
agentCode | Echoed from your request |
signalName | Echoed from your request |
model / modelProvider | Echoed from your request |
inputTokens / outputTokens | Echoed from your request |
quantity | Echoed (or 1 if you didn’t send it) |
totalCostUsd | Calculated cost in USD (string with 10 decimal places) |
eventId | UUID of the signal_events row — use this for lookups |
rawEventId | UUID of the raw_ingest_events audit row |
timestamp | When the event was processed |
Failed entry fields
| Field | Description |
|---|---|
record | The original record you sent (echoed back so you can identify it) |
code | Why it failed: NEEDS_COST_BACKFILL, INTERNAL_ERROR, or VALIDATION_ERROR |
stored | true = event is saved in the system (don’t retry). false = event was NOT saved (safe to retry). |
eventId | UUID of the signal_events row (only present when stored: true) |
rawEventId | UUID of the raw audit row (present for most failures) |
error | Human-readable description of what happened |
Error codes explained
| Code | Stored? | What happened | What to do |
|---|---|---|---|
NEEDS_COST_BACKFILL | Yes | The model+provider combination isn’t in the pricing table. The event is saved with cost: null. | Do NOT retry. Go to the MarginFront dashboard → Usage Events → “Needs attention” and map the model to a known one. Once mapped, cost is backfilled automatically and all future events with that model auto-resolve. |
INTERNAL_ERROR | No | Something broke on our side during processing. The event was not saved to signal_events. | Safe to retry. The raw audit row may exist (check rawEventId). |
VALIDATION_ERROR | No | Bad input — a required field is missing or has the wrong type. | Fix the request and resend. Check the error message for which field. |
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
| Param | Type | Default | Description |
|---|---|---|---|
startDate | ISO 8601 | 30 days ago | Filter events after this date |
endDate | ISO 8601 | now | Filter events before this date |
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.| Param | Type | Default | Description |
|---|---|---|---|
page | integer (≥ 1) | 1 | Page number. |
limit | integer (1-100) | 20 | Results per page. Capped at 100. |
customerId | UUID | — | Filter to events for one customer. Use MarginFront’s internal customer UUID (not your externalId). |
agentId | UUID | — | Filter to events for one agent. |
signalId | UUID | — | Filter to events for one signal. |
startDate | ISO 8601 string | — | Only events on or after this timestamp. |
endDate | ISO 8601 string | — | Only events on or before this timestamp. |
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:
| Value | Meaning |
|---|---|
"PROCESSED" | Cost calculated and stored. Ready to be included in invoices. |
"NEEDS_COST_BACKFILL" | Event stored but cost is null — the model+provider wasn’t found in the pricing table. Use GET /v1/events/needs-cost-backfill to find these and POST /v1/events/map-model to resolve. |
"PENDING" | Still being processed. Should transition within seconds. |
"ERROR" | Cost calculation failed for a reason other than an unknown model. Rare — inspect the event in the dashboard. |
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
| Status | Cause |
|---|---|
400 Bad Request | The batch is empty, has more than 100 records, or a record is missing a required field (customerExternalId, agentCode, signalName, model, or modelProvider). The response body tells you which field. |
401 Unauthorized | API key is missing or invalid. |
403 Forbidden | You used a publishable key (mf_pk_*). Usage recording requires a secret key (mf_sk_*). |
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.

