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 →

Release held usage

POST
/v1/usage-events/held/release
curl --request POST \
--url http://localhost:8080/v1/usage-events/held/release \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "external_customer_id": "example", "customer_id": "example" }'

Writes the held rows for one external customer ID as usage events for a customer, each under its original idempotency key and time, marked origin: backfill, in one transaction. A row that is already a usage event under the same key is not written again. Releasing twice writes nothing the second time.

Rows whose meter still does not exist, or whose time is after the customer’s current time, stay held. One call releases at most 5,000 rows; more_to_release counts the rest, and another call takes them. A released row counts toward the billing period its time falls in only if that period’s invoice has not been built yet; Hardcount does not move it onto a later invoice. Writes an audit log entry. Requires usage write permission.

Media typeapplication/json
object
external_customer_id
required

The external ID the rows were held under.

string
customer_id

Release into this customer (an hc_cus_ id) instead of the one whose external ID matches, for a user value that had a typo.

string

Example generated

{
"external_customer_id": "example",
"customer_id": "example"
}

Release result

Media typeapplication/json

The counts are what Hardcount found when the release wrote the rows, as for billing_status (see UsageBillingStatus). The invoice is the final answer.

object
customer_id
required

The customer the rows were ingested for.

string
released
required

Rows newly written as usage events.

integer
deduplicated
required

Rows that were already usage events under the same idempotency key (LiteLLM resent the call after the customer existed), and LiteLLM cache-write rows whose call is already billed with the writes inside its input row. Marked released, not written again.

integer
still_held
required

Rows for this external ID still held that a release cannot take yet: their meter does not exist yet, or their time is after the customer’s current time (a customer on a test clock).

integer
more_to_release
required

Rows for this external ID still held only because one release takes at most 5,000 rows. Release again to take them.

integer
in_open_period
required

Released rows in a billing period not invoiced yet that a price of its plan takes. Rows Hardcount does not check count here too: rows after the current period’s end, while the subscription continues, and rows after a trial’s end while the subscription still trials. Rows of a sum or count meter stamped before the end of this period’s spend-threshold invoice are not counted here: see pending_threshold_invoice and outside_open_period.

integer
no_price
required

Released rows in a billing period not invoiced yet whose plan prices the meter, but no price of that plan takes them (their dimensions do not include any price’s dimension_match).

integer
not_on_plan
required

Released rows in a billing period not invoiced yet whose plan does not price the meter.

integer
pending_threshold_invoice
required

Released rows of a sum or count meter stamped before the end of this period’s spend-threshold invoice, while that invoice is not paid. The period’s invoice counts them only if that invoice is voided before the period closes. A written-off invoice can still be voided.

integer
outside_open_period
required

Released rows outside every subscription’s time (before it started, in a free trial, after it ended or after its scheduled cancel, or in another, ended subscription’s time), and rows in a period already invoiced. Also rows of a sum or count meter stamped before the end of this period’s paid spend-threshold invoice. Bill them with a one-off invoice if they should be charged.

integer

Example generated

{
"customer_id": "example",
"released": 1,
"deduplicated": 1,
"still_held": 1,
"more_to_release": 1,
"in_open_period": 1,
"no_price": 1,
"not_on_plan": 1,
"pending_threshold_invoice": 1,
"outside_open_period": 1
}

validation_error: no customer has that external ID yet and no customer_id was given, or customer_id was 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"
}
}