Subscriptions
A Subscription links a Customer to a Plan and drives the billing cycle. It tracks the billing period, payment status, and charged-through date.
Subscription Fields
| Field | Type | Description |
|---|---|---|
id | string (UUIDv7) | Unique subscription ID |
customer_id | string | Owner customer |
plan_id | string | The plan being subscribed to |
status | enum | See lifecycle below |
currency | string | ISO 4217 billing currency |
period_start | datetime | Current billing period start |
period_end | datetime | Current billing period end |
charged_through_date | datetime | How far billing has been collected |
trial_days | integer | Trial days remaining at creation |
contract_id | string? | Linked enterprise contract (if any) |
Lifecycle States
| Status | Meaning |
|---|---|
Trialing | In trial period — not yet billed |
Active | Billing normally |
PastDue | Payment failed, in dunning |
Paused | Billing paused (admin action) |
Cancelled | Cancelled, active through period end |
Expired | Period ended, no renewal |
Create a Subscription
# [curl]
curl -X POST https://api.bill.sh/v1/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-sub-acme-startup" \
-d '{
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"plan_id": "01944b1f-0000-7000-8000-000000000002",
"currency": "USD",
"trial_days": 14
}'
# [Python]
import requests
resp = requests.post(
"https://api.bill.sh/v1/subscriptions",
headers={
"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": "create-sub-acme-startup",
},
json={
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"plan_id": "01944b1f-0000-7000-8000-000000000002",
"currency": "USD",
"trial_days": 14,
},
)
subscription = resp.json()
print(f"Subscription {subscription['id']} — {subscription['status']}")
// [Node.js]
const resp = await fetch("https://api.bill.sh/v1/subscriptions", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "create-sub-acme-startup",
},
body: JSON.stringify({
customer_id: "01944b1f-0000-7000-8000-000000000001",
plan_id: "01944b1f-0000-7000-8000-000000000002",
currency: "USD",
trial_days: 14,
}),
});
const subscription = await resp.json();
console.log(`Subscription ${subscription.id} — ${subscription.status}`);
// [Go]
body, _ := json.Marshal(map[string]interface{}{
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"plan_id": "01944b1f-0000-7000-8000-000000000002",
"currency": "USD",
"trial_days": 14,
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/v1/subscriptions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "create-sub-acme-startup")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var subscription map[string]interface{}
json.NewDecoder(resp.Body).Decode(&subscription)
fmt.Printf("Subscription %s — %s\n", subscription["id"], subscription["status"])
Get a Subscription
# [curl]
curl https://api.bill.sh/v1/subscriptions/01944b1f-0000-7000-8000-000000000003 \
-H "Authorization: Bearer $TOKEN"
# [Python]
resp = requests.get(
f"https://api.bill.sh/v1/subscriptions/{subscription_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
print(resp.json())
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/v1/subscriptions/${subscriptionId}`,
{ headers: { "Authorization": `Bearer ${TOKEN}` } }
);
const subscription = await resp.json();
console.log(subscription);
// [Go]
req, _ := http.NewRequest("GET",
"https://api.bill.sh/v1/subscriptions/"+subscriptionID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var subscription map[string]interface{}
json.NewDecoder(resp.Body).Decode(&subscription)
Cancel a Subscription
Cancellation is immediate in the system but the customer retains access through period_end:
# [curl]
curl -X POST https://api.bill.sh/v1/subscriptions/01944b1f-0000-7000-8000-000000000003/cancel \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cancel-acme-sub-2026-02" \
-d '{ "reason": "Customer requested cancellation" }'
# [Python]
resp = requests.post(
f"https://api.bill.sh/v1/subscriptions/{subscription_id}/cancel",
headers={
"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": f"cancel-{subscription_id}-2026-02",
},
json={"reason": "Customer requested cancellation"},
)
print(resp.json()) # {"status": "Cancelled", "period_end": "..."}
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/v1/subscriptions/${subscriptionId}/cancel`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": `cancel-${subscriptionId}-2026-02`,
},
body: JSON.stringify({ reason: "Customer requested cancellation" }),
}
);
const result = await resp.json();
console.log("Status:", result.status, "Active until:", result.period_end);
// [Go]
body, _ := json.Marshal(map[string]string{
"reason": "Customer requested cancellation",
})
req, _ := http.NewRequest("POST",
"https://api.bill.sh/v1/subscriptions/"+subscriptionID+"/cancel",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "cancel-"+subscriptionID+"-2026-02")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
Pause / Resume (Admin)
# [curl]
# Pause
curl -X POST https://api.bill.sh/admin/v1/subscriptions/$SUB_ID/pause \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Resume
curl -X POST https://api.bill.sh/admin/v1/subscriptions/$SUB_ID/resume \
-H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
# Pause
requests.post(
f"https://api.bill.sh/admin/v1/subscriptions/{sub_id}/pause",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
# Resume
requests.post(
f"https://api.bill.sh/admin/v1/subscriptions/{sub_id}/resume",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
// [Node.js]
// Pause
await fetch(`https://api.bill.sh/admin/v1/subscriptions/${subId}/pause`, {
method: "POST",
headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
});
// Resume
await fetch(`https://api.bill.sh/admin/v1/subscriptions/${subId}/resume`, {
method: "POST",
headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
});
// [Go]
// Pause
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/subscriptions/"+subID+"/pause", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
http.DefaultClient.Do(req)
// Resume
req2, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/subscriptions/"+subID+"/resume", nil)
req2.Header.Set("Authorization", "Bearer "+adminToken)
http.DefaultClient.Do(req2)
Trigger Manual Billing (Admin)
Force a billing run outside the normal cycle — useful for testing or ad-hoc charges:
# [curl]
curl -X POST https://api.bill.sh/admin/v1/subscriptions/$SUB_ID/bill \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Idempotency-Key: manual-bill-$SUB_ID-2026-02"
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/subscriptions/{sub_id}/bill",
headers={
"Authorization": f"Bearer {ADMIN_TOKEN}",
"Idempotency-Key": f"manual-bill-{sub_id}-2026-02",
},
)
resp.raise_for_status()
print("Billing triggered")
// [Node.js]
await fetch(`https://api.bill.sh/admin/v1/subscriptions/${subId}/bill`, {
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Idempotency-Key": `manual-bill-${subId}-2026-02`,
},
});
// [Go]
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/subscriptions/"+subID+"/bill", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Idempotency-Key", "manual-bill-"+subID+"-2026-02")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
List Invoices for a Subscription
# [curl]
curl https://api.bill.sh/v1/subscriptions/$SUB_ID/invoices \
-H "Authorization: Bearer $TOKEN"
# [Python]
resp = requests.get(
f"https://api.bill.sh/v1/subscriptions/{sub_id}/invoices",
headers={"Authorization": f"Bearer {TOKEN}"},
)
for inv in resp.json():
print(f"{inv.get('invoice_number', 'Draft')} — {inv['status']} — ${int(inv['total_nanos']) / 1e12:.2f}")
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/v1/subscriptions/${subId}/invoices`,
{ headers: { "Authorization": `Bearer ${TOKEN}` } }
);
const invoices = await resp.json();
for (const inv of invoices) {
const amount = (BigInt(inv.total_nanos) / BigInt(1e12)).toString();
console.log(`${inv.invoice_number ?? "Draft"} — ${inv.status} — $${amount}`);
}
// [Go]
req, _ := http.NewRequest("GET",
"https://api.bill.sh/v1/subscriptions/"+subID+"/invoices", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var invoices []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&invoices)
for _, inv := range invoices {
fmt.Printf("%v — %v\n", inv["invoice_number"], inv["status"])
}