Quick Start
This guide walks through the full billing cycle: create a customer, subscribe them to a plan, record usage, and generate an invoice — all using the REST API.
Base URL: https://api.bill.sh
Step 1: Create a Customer
# [curl]
curl -X POST https://api.bill.sh/admin/v1/customers \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Acme Corp",
"email": "billing@acmecorp.com",
"currency": "USD",
"account_type": "Organization"
}'
# [Python]
import requests
resp = requests.post(
"https://api.bill.sh/admin/v1/customers",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={
"display_name": "Acme Corp",
"email": "billing@acmecorp.com",
"currency": "USD",
"account_type": "Organization",
},
)
customer = resp.json()
customer_id = customer["id"]
print(f"Created customer: {customer_id}")
// [Node.js]
const resp = await fetch("https://api.bill.sh/admin/v1/customers", {
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
display_name: "Acme Corp",
email: "billing@acmecorp.com",
currency: "USD",
account_type: "Organization",
}),
});
const customer = await resp.json();
const customerId = customer.id;
console.log("Created customer:", customerId);
// [Go]
import (
"bytes"
"encoding/json"
"net/http"
)
body, _ := json.Marshal(map[string]string{
"display_name": "Acme Corp",
"email": "billing@acmecorp.com",
"currency": "USD",
"account_type": "Organization",
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/customers", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var customer map[string]interface{}
json.NewDecoder(resp.Body).Decode(&customer)
customerID := customer["id"].(string)
Response:
{
"id": "01944b1f-0000-7000-8000-000000000001",
"display_name": "Acme Corp",
"account_type": "Organization",
"parent_id": null
}
Save the id — you’ll need it for subsequent calls.
Step 2: Subscribe the Customer to a Plan
First, list available plans to find the plan_id:
# [curl]
curl https://api.bill.sh/admin/v1/catalog/plans \
-H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
plans_resp = requests.get(
"https://api.bill.sh/admin/v1/catalog/plans",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
plans = plans_resp.json()
plan_id = plans[0]["id"] # pick the right plan for your use case
// [Node.js]
const plansResp = await fetch("https://api.bill.sh/admin/v1/catalog/plans", {
headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
});
const plans = await plansResp.json();
const planId = plans[0].id;
// [Go]
req, _ := http.NewRequest("GET", "https://api.bill.sh/admin/v1/catalog/plans", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
// decode plans list...
Then create the subscription:
# [curl]
curl -X POST https://api.bill.sh/v1/subscriptions \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: onboard-acme-sub-001" \
-d '{
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"plan_id": "01944b1f-0000-7000-8000-000000000002",
"currency": "USD",
"trial_days": 14
}'
# [Python]
sub_resp = requests.post(
"https://api.bill.sh/v1/subscriptions",
headers={
"Authorization": f"Bearer {CUSTOMER_TOKEN}",
"Idempotency-Key": "onboard-acme-sub-001",
},
json={
"customer_id": customer_id,
"plan_id": plan_id,
"currency": "USD",
"trial_days": 14,
},
)
subscription = sub_resp.json()
subscription_id = subscription["id"]
print(f"Subscription: {subscription_id}, status: {subscription['status']}")
// [Node.js]
const subResp = await fetch("https://api.bill.sh/v1/subscriptions", {
method: "POST",
headers: {
"Authorization": `Bearer ${CUSTOMER_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "onboard-acme-sub-001",
},
body: JSON.stringify({
customer_id: customerId,
plan_id: planId,
currency: "USD",
trial_days: 14,
}),
});
const subscription = await subResp.json();
const subscriptionId = subscription.id;
console.log(`Subscription: ${subscriptionId}, status: ${subscription.status}`);
// [Go]
body, _ := json.Marshal(map[string]interface{}{
"customer_id": customerID,
"plan_id": planID,
"currency": "USD",
"trial_days": 14,
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/v1/subscriptions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+customerToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "onboard-acme-sub-001")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var subscription map[string]interface{}
json.NewDecoder(resp.Body).Decode(&subscription)
subscriptionID := subscription["id"].(string)
Response:
{
"id": "01944b1f-0000-7000-8000-000000000003",
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"plan_id": "01944b1f-0000-7000-8000-000000000002",
"status": "Trialing",
"currency": "USD",
"period_start": "2026-02-28T00:00:00Z",
"period_end": "2026-03-28T00:00:00Z"
}
Step 3: Record Usage Events
Send usage events as they occur. Each event is a CloudEvents-compatible JSON object:
# [curl]
curl -X POST https://api.bill.sh/v1/events \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: evt-acme-2026-02-28-001" \
-d '{
"id": "evt-acme-2026-02-28-001",
"event_type": "api.request",
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"subscription_id": "01944b1f-0000-7000-8000-000000000003",
"timestamp": "2026-02-28T12:34:56Z",
"properties": {
"model": "gpt-4",
"tokens": 1500,
"region": "us-east-1"
}
}'
# [Python]
import uuid
event_id = f"evt-acme-{uuid.uuid4()}"
event_resp = requests.post(
"https://api.bill.sh/v1/events",
headers={
"Authorization": f"Bearer {CUSTOMER_TOKEN}",
"Idempotency-Key": event_id,
},
json={
"id": event_id,
"event_type": "api.request",
"customer_id": customer_id,
"subscription_id": subscription_id,
"timestamp": "2026-02-28T12:34:56Z",
"properties": {
"model": "gpt-4",
"tokens": 1500,
"region": "us-east-1",
},
},
)
print(event_resp.json()) # {"accepted": true, "event_id": "..."}
// [Node.js]
import { randomUUID } from "crypto";
const eventId = `evt-acme-${randomUUID()}`;
const eventResp = await fetch("https://api.bill.sh/v1/events", {
method: "POST",
headers: {
"Authorization": `Bearer ${CUSTOMER_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": eventId,
},
body: JSON.stringify({
id: eventId,
event_type: "api.request",
customer_id: customerId,
subscription_id: subscriptionId,
timestamp: new Date().toISOString(),
properties: { model: "gpt-4", tokens: 1500, region: "us-east-1" },
}),
});
console.log(await eventResp.json()); // { accepted: true, event_id: "..." }
// [Go]
import "github.com/google/uuid"
eventID := "evt-acme-" + uuid.New().String()
body, _ := json.Marshal(map[string]interface{}{
"id": eventID,
"event_type": "api.request",
"customer_id": customerID,
"subscription_id": subscriptionID,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"properties": map[string]interface{}{
"model": "gpt-4",
"tokens": 1500,
"region": "us-east-1",
},
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/v1/events", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+customerToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", eventID)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
Response:
{
"accepted": true,
"event_id": "evt-acme-2026-02-28-001"
}
You can check the usage summary at any time:
# [curl]
curl https://api.bill.sh/v1/subscriptions/01944b1f-0000-7000-8000-000000000003/usage \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
# [Python]
usage_resp = requests.get(
f"https://api.bill.sh/v1/subscriptions/{subscription_id}/usage",
headers={"Authorization": f"Bearer {CUSTOMER_TOKEN}"},
)
usage = usage_resp.json()
for meter in usage["meters"]:
print(f"{meter['event_type']} ({meter['aggregation']}): {meter['value']}")
// [Node.js]
const usageResp = await fetch(
`https://api.bill.sh/v1/subscriptions/${subscriptionId}/usage`,
{ headers: { "Authorization": `Bearer ${CUSTOMER_TOKEN}` } }
);
const usage = await usageResp.json();
for (const meter of usage.meters) {
console.log(`${meter.event_type} (${meter.aggregation}): ${meter.value}`);
}
// [Go]
req, _ := http.NewRequest("GET",
"https://api.bill.sh/v1/subscriptions/"+subscriptionID+"/usage", nil)
req.Header.Set("Authorization", "Bearer "+customerToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var usage map[string]interface{}
json.NewDecoder(resp.Body).Decode(&usage)
Step 4: Generate an Invoice
At the end of the billing period, trigger billing for the subscription:
# [curl]
curl -X POST https://api.bill.sh/admin/v1/subscriptions/01944b1f-0000-7000-8000-000000000003/bill \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Idempotency-Key: bill-acme-2026-02"
# [Python]
bill_resp = requests.post(
f"https://api.bill.sh/admin/v1/subscriptions/{subscription_id}/bill",
headers={
"Authorization": f"Bearer {ADMIN_TOKEN}",
"Idempotency-Key": f"bill-acme-2026-02",
},
)
bill_resp.raise_for_status()
// [Node.js]
await fetch(
`https://api.bill.sh/admin/v1/subscriptions/${subscriptionId}/bill`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Idempotency-Key": "bill-acme-2026-02",
},
}
);
// [Go]
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/subscriptions/"+subscriptionID+"/bill", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Idempotency-Key", "bill-acme-2026-02")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
Then finalize the draft invoice to assign an invoice number:
# [curl]
# List invoices to find the Draft
curl "https://api.bill.sh/admin/v1/invoices?customer_id=01944b1f-0000-7000-8000-000000000001" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Finalize it
curl -X POST https://api.bill.sh/admin/v1/invoices/01944b1f-0000-7000-8000-000000000004/finalize \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Idempotency-Key: finalize-inv-2026-02"
# [Python]
# Find the Draft invoice
invoices_resp = requests.get(
"https://api.bill.sh/admin/v1/invoices",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
params={"customer_id": customer_id, "status": "Draft"},
)
draft = invoices_resp.json()[0]
invoice_id = draft["id"]
# Finalize it
finalize_resp = requests.post(
f"https://api.bill.sh/admin/v1/invoices/{invoice_id}/finalize",
headers={
"Authorization": f"Bearer {ADMIN_TOKEN}",
"Idempotency-Key": f"finalize-inv-{invoice_id}",
},
)
invoice = finalize_resp.json()
print(f"Invoice {invoice['invoice_number']} is now {invoice['status']}")
// [Node.js]
// Find the Draft invoice
const invoicesResp = await fetch(
`https://api.bill.sh/admin/v1/invoices?customer_id=${customerId}&status=Draft`,
{ headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` } }
);
const invoices = await invoicesResp.json();
const invoiceId = invoices[0].id;
// Finalize it
const finalizeResp = await fetch(
`https://api.bill.sh/admin/v1/invoices/${invoiceId}/finalize`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Idempotency-Key": `finalize-inv-${invoiceId}`,
},
}
);
const invoice = await finalizeResp.json();
console.log(`Invoice ${invoice.invoice_number} is now ${invoice.status}`);
// [Go]
// Find the Draft invoice
req, _ := http.NewRequest("GET",
"https://api.bill.sh/admin/v1/invoices?customer_id="+customerID+"&status=Draft", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var invoices []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&invoices)
invoiceID := invoices[0]["id"].(string)
// Finalize it
req2, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/invoices/"+invoiceID+"/finalize", nil)
req2.Header.Set("Authorization", "Bearer "+adminToken)
req2.Header.Set("Idempotency-Key", "finalize-inv-"+invoiceID)
resp2, _ := http.DefaultClient.Do(req2)
defer resp2.Body.Close()
var invoice map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&invoice)
fmt.Printf("Invoice %s is now %s\n", invoice["invoice_number"], invoice["status"])
Response:
{
"id": "01944b1f-0000-7000-8000-000000000004",
"status": "Open",
"invoice_number": "INV-000001"
}
The invoice is now Open and ready for payment collection. Stripe will automatically collect payment if the customer has a payment method on file.
What’s Next?
- Set up webhooks to receive real-time billing events
- Configure spend alerts to notify customers before they hit limits
- Read about idempotency to make your integration bulletproof
- Explore the interactive API explorer