Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

FieldTypeDescription
idstring (UUIDv7)Unique subscription ID
customer_idstringOwner customer
plan_idstringThe plan being subscribed to
statusenumSee lifecycle below
currencystringISO 4217 billing currency
period_startdatetimeCurrent billing period start
period_enddatetimeCurrent billing period end
charged_through_datedatetimeHow far billing has been collected
trial_daysintegerTrial days remaining at creation
contract_idstring?Linked enterprise contract (if any)

Lifecycle States

StatusMeaning
TrialingIn trial period — not yet billed
ActiveBilling normally
PastDuePayment failed, in dunning
PausedBilling paused (admin action)
CancelledCancelled, active through period end
ExpiredPeriod 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"])
}