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 →

Create subscription

POST
/v1/subscriptions
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" }'
Idempotency-Key
string

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.

Media typeapplication/json
object
code
required
string
display_name
required
string
customer_id
required
string
items
required

Plan items — a subscription bills one or more plans.

Array<object>
object
plan_id
required
string
quantity
integer
default: 1
billing_time
string
default: calendar
Allowed values: calendar anniversary
trial_days

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.

integer
<= 730
status

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.

string
Allowed values: draft

Subscription created

Media typeapplication/json
object
id
required
string
tenant_id
string
code
required
string
display_name
required
string
customer_id
required
string
items

The subscription’s priced lines. Each item names its plan.

Array<object>

One priced line of a subscription. It pairs a plan with a quantity.

object
id
required
string
tenant_id
string
subscription_id
required
string
plan_id
required
string
quantity
required
integer format: int64
metadata

The item’s stored metadata. Absent when empty.

object
key
additional properties
any
pending_plan_id

Plan the item moves to at pending_plan_effective_at. Absent when no change is scheduled.

string
pending_plan_effective_at
string format: date-time
plan_changed_at

When the plan was last changed immediately. Absent until the first such change.

string format: date-time
created_at
required
string format: date-time
updated_at
required
string format: date-time
status
required
string
Allowed values: draft trialing active canceled
billing_time
required
string
Allowed values: calendar anniversary
trial_start_at
string format: date-time
trial_end_at
string format: date-time
started_at
string format: date-time
activated_at
string format: date-time
canceled_at

When the subscription was canceled. Absent until then.

string format: date-time
cancel_at

A scheduled cancel time, if one is set.

string format: date-time
cancel_at_period_end
required

True when the subscription cancels at the end of the current period.

boolean
cancel_effective_at

When a scheduled cancel takes effect. Absent when no cancel is scheduled.

string format: date-time
pause_collection

Present on a subscription while collection is paused. New invoices are still created, but stay drafts and are not charged.

object
behavior
required
string
Allowed values: keep_as_draft
resumes_at

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.

string format: date-time
set_by
required

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.

string
Allowed values: operator dunning
billing_thresholds

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
amount_gte

Usage threshold in cents, compared with the period’s usage so far (the base fee is not counted). Absent when not set.

integer format: int64
item_thresholds
required
Array<object>
object
subscription_item_id
required
string
usage_gte
required

Usage quantity threshold, as a decimal string.

string
current_billing_period_start
string format: date-time
current_billing_period_end

End of the current period (exclusive).

string format: date-time
current_billing_period_display

The current period as text, with the last day included, in the tenant’s billing time zone.

string
next_billing_at
string format: date-time
billing_anchor_day

Day of the month the subscription bills on (1-31). Absent for calendar-monthly subscriptions.

integer
test_clock_id
string
billing_failure

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
attempts
required

Failed attempts on the work that is due now. At least 1.

integer
max_attempts
required

The attempt at which the subscription parks (8).

integer
last_error
required

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.

string
failed_at
required

When the latest attempt failed.

string format: date-time
next_attempt_at

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.

string format: date-time
parked
required

True when automatic attempts have stopped.

boolean
created_at
required
string format: date-time
updated_at
required
string format: date-time

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.

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