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

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?