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 →

Ingest LiteLLM spend logs

POST
/v1/integrations/litellm/spend
curl --request POST \
--url http://localhost:8080/v1/integrations/litellm/spend \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "id": "example", "status": "example", "cache_hit": true, "call_type": "example", "model": "example", "custom_llm_provider": "example", "user": "example", "end_user": "example", "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "prompt_tokens_details": { "cached_tokens": 1, "cache_creation_tokens": 1, "cache_creation_token_details": { "ephemeral_5m_input_tokens": 1, "ephemeral_1h_input_tokens": 1 } }, "cache_read_input_tokens": 1, "cache_creation_input_tokens": 1 }, "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "response_cost": 1, "cost_breakdown": { "input_cost": 1, "output_cost": 1, "cache_read_cost": 1, "cache_creation_cost": 1, "tool_usage_cost": 1, "total_cost": 1 }, "startTime": 1, "endTime": 1, "metadata": {} }'

Target for LiteLLM Proxy’s generic_api logging callback. Accepts one LiteLLM log record, a JSON array of records, or {"events": [...]}. Each completion, chat, text-completion or embedding record becomes up to five usage events on the tokens meter, one per token_type dimension: input (prompt tokens not read from or written to the cache), cache_read, cache_write_5m, cache_write_1h and output. A record’s events are recorded in one transaction. Each event also carries model (a model family name when recognised), model_raw and provider, plus team_id and request_tags from metadata when present. Other call types, records with no token counts, failed calls and LiteLLM response-cache hits are counted as skipped.

The customer is the record’s user or end_user (or metadata.user_api_key_end_user_id), matched against the customer’s external_id. Event idempotency keys are the record id plus the token type, so a resent record is counted as deduplicated, not billed twice.

A token row whose customer or meter does not exist yet is held, not billed and not lost: it is counted in held and listed by GET /v1/usage-events/held until you release or discard it. A resent row that is already held is counted as deduplicated. A record with no user or no id, or an invalid event, is listed in errors and the rest of the batch still goes in, with status 200. If the usage store cannot be reached, the whole request fails with 503 so LiteLLM retries it. Requires usage write permission.

Media typeapplication/json
One of:

The fields of a LiteLLM StandardLoggingPayload that Hardcount reads. Other fields are accepted and ignored.

object
id

LiteLLM’s call id. Used to build the events’ idempotency keys.

string
status

A failure record (a call the provider refused or that timed out) is skipped, whatever token counts it carries.

string
cache_hit

true when LiteLLM answered from its own response cache. The record is skipped and counted in cache_hits: the provider charged nothing.

boolean
nullable
call_type

A record bills on its token counts unless its call type is one Hardcount does not bill (images, audio, realtime, moderation, batches, files, A2A, response retrieval, …); those are skipped, and a skipped record that carries tokens is logged.

string
model
string
custom_llm_provider
string
user

The customer’s external_id.

string
end_user

Where LiteLLM Proxy puts the caller’s user. Used when user is empty.

string
usage
object
prompt_tokens

Includes cache-read tokens.

integer format: int64
completion_tokens
integer format: int64
total_tokens
integer format: int64
prompt_tokens_details
object
cached_tokens
integer format: int64
cache_creation_tokens
integer format: int64
cache_creation_token_details

Anthropic’s cache writes by cache lifetime. They become cache_write_5m and cache_write_1h events, taken out of input. Without this split, or when it does not add up to the cache-write count, the writes stay in input.

object
ephemeral_5m_input_tokens
integer format: int64
nullable
ephemeral_1h_input_tokens
integer format: int64
nullable
cache_read_input_tokens
integer format: int64
cache_creation_input_tokens

Cache-write tokens, included in prompt_tokens. Split into cache_write_5m and cache_write_1h by prompt_tokens_details.cache_creation_token_details.

integer format: int64
prompt_tokens

Used when there is no usage object.

integer format: int64
completion_tokens
integer format: int64
total_tokens
integer format: int64
response_cost

Accepted but not stored.

number
cost_breakdown

The provider’s own cost, in dollars. When present, each event’s provider cost comes from its part (input, output, cache read or cache write) instead of the provider-costs table. A part that is missing or negative falls back to the table. input_cost includes cache reads and cache writes, so the input event gets input_cost minus cache_read_cost and cache_creation_cost. If there were cache reads but no cache_read_cost, or split-off cache writes but no cache_creation_cost, the input-side events use the provider-costs table. With 1-hour cache writes they always do: LiteLLM prices cache_creation_cost at the 5-minute rate.

object
input_cost
number
output_cost
number
cache_read_cost
number
cache_creation_cost
number
tool_usage_cost
number
total_cost
number
startTime

Unix seconds. Event time when endTime is absent.

number
endTime

Unix seconds. The event time. With neither, the customer’s current time is used.

number
metadata

team_id and request_tags become event dimensions; usage_object is read for token counts; other keys are ignored.

object
key
additional properties
any

Example generated

{
"id": "example",
"status": "example",
"cache_hit": true,
"call_type": "example",
"model": "example",
"custom_llm_provider": "example",
"user": "example",
"end_user": "example",
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 1,
"prompt_tokens_details": {
"cached_tokens": 1,
"cache_creation_tokens": 1,
"cache_creation_token_details": {
"ephemeral_5m_input_tokens": 1,
"ephemeral_1h_input_tokens": 1
}
},
"cache_read_input_tokens": 1,
"cache_creation_input_tokens": 1
},
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 1,
"response_cost": 1,
"cost_breakdown": {
"input_cost": 1,
"output_cost": 1,
"cache_read_cost": 1,
"cache_creation_cost": 1,
"tool_usage_cost": 1,
"total_cost": 1
},
"startTime": 1,
"endTime": 1,
"metadata": {}
}

The batch was processed. Check errors for records that were not recorded.

Media typeapplication/json
object
accepted
required

Usage events newly recorded.

integer
deduplicated
required

Usage events already recorded, or already held, by an earlier delivery.

integer
held
required

Usage events kept because their customer or meter does not exist yet. Not billed until released with POST /v1/usage-events/held/release.

integer
skipped
required

Records that bill nothing - a call type with no tokens, zero tokens, a failed call (status failure) or a LiteLLM response-cache hit.

integer
cache_hits
required

The LiteLLM response-cache hits among skipped. LiteLLM’s own spend counts them as $0.

integer
after_close
required

Of accepted, the events answered billing_status: after_close: their time was already invoiced. A replay never counts here.

integer
no_price
required

Of accepted, the events no price of the subscription’s plan takes (billing_status: no_price), such as a model no price names. A replay never counts here.

integer
not_on_plan
required

Of accepted, the events whose meter the subscription’s plan does not price (billing_status: not_on_plan). A replay never counts here.

integer
outside_subscription
required

Of accepted, the events whose time no subscription of the customer bills (billing_status: outside_subscription). A replay never counts here.

integer
errors

One entry per record that was not recorded. A record’s events are recorded together or not at all.

Array<object>
object
id
required

The LiteLLM record id.

string
error
required
string

Example generated

{
"accepted": 1,
"deduplicated": 1,
"held": 1,
"skipped": 1,
"cache_hits": 1,
"after_close": 1,
"no_price": 1,
"not_on_plan": 1,
"outside_subscription": 1,
"errors": [
{
"id": "example",
"error": "example"
}
]
}

invalid_request: empty body or not valid JSON.

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"
}
}

ingest_unavailable: the usage store could not be reached. Retry the whole batch; events already recorded come back as deduplicated.

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"
}
}