Create subscription
const url = 'http://localhost:8080/v1/subscriptions';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"code":"example","display_name":"example","customer_id":"example","items":[{"plan_id":"example","quantity":1}],"billing_time":"calendar","trial_days":1,"status":"draft"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8080/v1/subscriptions \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "code": "example", "display_name": "example", "customer_id": "example", "items": [ { "plan_id": "example", "quantity": 1 } ], "billing_time": "calendar", "trial_days": 1, "status": "draft" }'Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Makes the call safe to retry. Send a unique value (a UUID works) and
the same value on every retry. For 24 hours, a repeat with the same
key, method, path and body gets the first response again, with an
Idempotent-Replayed: true header, instead of running again. The
error cases are in docs/api-surface.md. The usage routes do not take
it: each usage event carries its own idempotency_key.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Plan items — a subscription bills one or more plans.
object
Days of free trial; 0 means no trial. Above 0, the
subscription is trialing from now and its first paid
period starts on the day the trial ends.
Leave it out and the subscription starts now. Without
trial_days it is active, its first period starts
today in the account’s billing time zone (the clock’s
day for a customer on a test clock), and a non-zero base
fee billed in advance is invoiced now. With trial_days
above 0 it is trialing from now. Send draft to create it
without starting it: it bills nothing until you activate
it with POST /v1/subscriptions/{id}/activate. A draft
cannot have a trial (trial_days above 0). An empty or
null status is refused.
Responses
Section titled “Responses”Subscription created
object
The subscription’s priced lines. Each item names its plan.
One priced line of a subscription. It pairs a plan with a quantity.
object
The item’s stored metadata. Absent when empty.
object
Plan the item moves to at pending_plan_effective_at. Absent when no change is scheduled.
When the plan was last changed immediately. Absent until the first such change.
When the subscription was canceled. Absent until then.
A scheduled cancel time, if one is set.
True when the subscription cancels at the end of the current period.
When a scheduled cancel takes effect. Absent when no cancel is scheduled.
Present on a subscription while collection is paused. New invoices are still created, but stay drafts and are not charged.
object
When collection resumes. Absent when the pause lasts until it is cleared. On a pause set by dunning, it is when Hardcount recorded the payment of the invoice the pause was set for.
Who paused collection. operator: you, through the API or the
dashboard; the pause lasts until you resume it or until
resumes_at. dunning: a dunning policy’s pause final action.
A dunning pause lifts by itself once the invoice whose dunning
set it is paid, even if another invoice is still owed. Periods
that ended during the pause keep their draft invoices.
Limits that make Hardcount invoice before the period ends. A threshold invoice bills the period’s usage so far, and no base fee. The period keeps its dates, the base fee is billed as it would be with no threshold, and the period’s next invoice bills the rest of the usage.
object
Usage threshold in cents, compared with the period’s usage so far (the base fee is not counted). Absent when not set.
object
Usage quantity threshold, as a decimal string.
End of the current period (exclusive).
The current period as text, with the last day included, in the tenant’s billing time zone.
Day of the month the subscription bills on (1-31). Absent for calendar-monthly subscriptions.
Present on a subscription while the billing run is failing on the
work that subscription has due now (a period close, or a scheduled
mid-period cancel). Absent otherwise: a subscription that bills
cleanly, or whose due work changed, carries no billing_failure.
After each failed attempt the billing run waits before the next one:
15 minutes, 1 hour, 4 hours, 12 hours, then 24 hours three times.
The eighth failure parks the subscription (parked: true, no
next_attempt_at). A parked subscription is not attempted again
until POST /v1/subscriptions/{id}/retry-billing is called.
Never set on a subscription attached to a test clock.
object
Failed attempts on the work that is due now. At least 1.
The attempt at which the subscription parks (8).
Why the latest attempt failed: a sentence an operator can act on, or a generic line carrying the subscription id as the reference to search the server log for. Never raw error text.
When the latest attempt failed.
The billing run does not attempt the subscription before this instant. The attempt happens on the first run at or after it, so on an hourly schedule it can be up to an hour later. Absent when parked.
True when automatic attempts have stopped.
Example
{ "status": "draft", "billing_time": "calendar", "pause_collection": { "behavior": "keep_as_draft", "set_by": "operator" }}A field failed validation (param names it), for example
trial_days outside 0 to 730, status other than draft
(empty and null included), or trial_days above 0 with
status: draft. Or the body has an unknown
field (param names it; the body must use only the fields
listed here). start_now is such a field since 2026-10-23: a
subscription starts now unless status is draft.
object
object
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.
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.
Example
{ "error": { "type": "invalid_request_error" }}