Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

Per-customer margin report (operator only)

GET
/v1/customers/{id}/margin
curl --request GET \
--url http://localhost:8080/v1/customers/example/margin \
--header 'Authorization: Bearer <token>'

Rated usage revenue vs stamped provider COGS for a window (ADR-079). A usage unit-economics view — base fees, credits, and taxes are not included. Per-model margin renders only where the price that billed the usage names the model; other revenue is reported unattributed, never allocated by heuristic.

The window is [from, to): from inclusive, to exclusive. A missing bound defaults to the last 30 days ending at the customer’s current time, which is the test clock’s frozen time when the customer is on a test clock. from and to take RFC 3339 instants only (no bare dates), and from must be before to.

id
required
string
from
string format: date-time

Window start, inclusive. RFC 3339. Default is 30 days before the customer’s current time.

to
string format: date-time

Window end, exclusive. RFC 3339. Default is the customer’s current time (test clock time for a customer on a test clock).

Margin report

Media typeapplication/json
object
customer_id
required
string
from
required
string format: date-time
to
required
string format: date-time
revenue_cents
required

Total rated usage revenue in the window, an estimate at each subscription’s current prices. Only active and trialing subscriptions are counted.

integer format: int64
revenue_currency

Currency of revenue_cents (the customer’s price rules). Omitted when the window’s revenue spans more than one currency or the customer has no priced meter.

string
cost_cents
required

Total stamped provider cost in US cents, as a decimal string (e.g. “275.75”).

string
cost_currency
required

Currency of cost_cents. Always USD.

string
margin_bps

(revenue − cost) / revenue in basis points. Omitted when revenue is 0, when revenue_currency is not cost_currency, when costed_events is 0 (no provider cost known), or when unresolved_events is above 0 (part of the cost unknown, so the margin would read too high).

integer format: int64
by_model
required
Array<object>
object
model
required
string
cost_cents
required

This model’s stamped provider cost in US cents, as a decimal string.

string
revenue_cents
integer format: int64
attributed
required

True only when the price that billed the usage names this model — margin is never heuristically allocated.

boolean
margin_bps

This model’s (revenue − cost) / revenue in basis points, by the same formula as the report’s margin_bps. Omitted whenever the report’s margin_bps is omitted for currency or unresolved_events, and when the row is not attributed or has no provider cost of its own.

integer format: int64
unattributed_revenue_cents
required
integer format: int64
unresolved_events
required

Token events with costable dims but no cost — no rate matched (add a rate to cost new events), or the cost, quantity × unit_cost_cents, was too large to record (about $9.2 trillion or more, either sign).

integer format: int64
costed_events
required

Events in the window that carry a stamped provider cost (zero cost included). 0 means the cost side is unknown.

integer format: int64
cost_rates_configured
required

Whether the tenant has any provider cost rate. False with costed_events 0 means no rates are set up.

boolean

Example generated

{
"customer_id": "example",
"from": "2026-04-15T12:00:00Z",
"to": "2026-04-15T12:00:00Z",
"revenue_cents": 1,
"revenue_currency": "example",
"cost_cents": "example",
"cost_currency": "example",
"margin_bps": 1,
"by_model": [
{
"model": "example",
"cost_cents": "example",
"revenue_cents": 1,
"attributed": true,
"margin_bps": 1
}
],
"unattributed_revenue_cents": 1,
"unresolved_events": 1,
"costed_events": 1,
"cost_rates_configured": true
}

from or to is not RFC 3339, or from is not before to.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

Customer not found.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

The window is longer than one year.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}