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

Products & Plans

The product catalog defines what you sell. It has three layers:

Product → Plan → SKU (price)

Products

A Product is a named offering (e.g., “API Platform”, “Storage”, “Support Tier”).

# [curl]
# List products
curl https://api.bill.sh/admin/v1/catalog/products \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Create a product
curl -X POST https://api.bill.sh/admin/v1/catalog/products \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API Platform",
    "description": "Usage-based API access",
    "tags": ["api", "core"]
  }'
# [Python]
import requests

# List products
products = requests.get(
    "https://api.bill.sh/admin/v1/catalog/products",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
).json()

# Create a product
resp = requests.post(
    "https://api.bill.sh/admin/v1/catalog/products",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "name": "API Platform",
        "description": "Usage-based API access",
        "tags": ["api", "core"],
    },
)
product = resp.json()
product_id = product["id"]
print(f"Product: {product_id}")
// [Node.js]
// List products
const productsResp = await fetch("https://api.bill.sh/admin/v1/catalog/products", {
  headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
});
const products = await productsResp.json();

// Create a product
const resp = await fetch("https://api.bill.sh/admin/v1/catalog/products", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "API Platform",
    description: "Usage-based API access",
    tags: ["api", "core"],
  }),
});
const product = await resp.json();
console.log("Product:", product.id);
// [Go]
// List products
req, _ := http.NewRequest("GET", "https://api.bill.sh/admin/v1/catalog/products", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var products []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&products)

// Create a product
body, _ := json.Marshal(map[string]interface{}{
    "name":        "API Platform",
    "description": "Usage-based API access",
    "tags":        []string{"api", "core"},
})
req2, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/catalog/products",
    bytes.NewReader(body))
req2.Header.Set("Authorization", "Bearer "+adminToken)
req2.Header.Set("Content-Type", "application/json")
resp2, _ := http.DefaultClient.Do(req2)
defer resp2.Body.Close()
var product map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&product)

Plans

A Plan bundles one or more SKUs together into a subscribable offering.

# [curl]
curl -X POST https://api.bill.sh/admin/v1/catalog/plans \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Startup",
    "description": "For teams up to 10 users",
    "billing_cadence": "Monthly",
    "trial_days": 14
  }'
# [Python]
resp = requests.post(
    "https://api.bill.sh/admin/v1/catalog/plans",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "name": "Startup",
        "description": "For teams up to 10 users",
        "billing_cadence": "Monthly",
        "trial_days": 14,
    },
)
plan = resp.json()
plan_id = plan["id"]
print(f"Plan ID: {plan_id}")
// [Node.js]
const resp = await fetch("https://api.bill.sh/admin/v1/catalog/plans", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Startup",
    description: "For teams up to 10 users",
    billing_cadence: "Monthly",
    trial_days: 14,
  }),
});
const plan = await resp.json();
console.log("Plan ID:", plan.id);
// [Go]
body, _ := json.Marshal(map[string]interface{}{
    "name":             "Startup",
    "description":      "For teams up to 10 users",
    "billing_cadence":  "Monthly",
    "trial_days":       14,
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/catalog/plans",
    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 plan map[string]interface{}
json.NewDecoder(resp.Body).Decode(&plan)
fmt.Println("Plan ID:", plan["id"])

SKUs (Price Points)

A SKU defines how a plan charges. Supported pricing models:

ModelDescriptionUse case
FlatFixed monthly feeSeat license, base platform fee
PerUnitPer-unit price × quantityPer API call, per GB
GraduatedTiered pricing, each tier applies to quantity in that tierUsage-based
VolumeAll units at the tier price that the total quantity falls intoVolume discounts
PackagePrice per bundle of N unitsCredits pack
CommittedMinimum commit + overage rateEnterprise committed use
# [curl]
curl -X POST https://api.bill.sh/admin/v1/catalog/skus \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "plan_id": "01944b1f-0000-7000-8000-000000000002",
    "name": "API Requests",
    "pricing_model": "PerUnit",
    "unit_amount_nanos": "1000000",
    "meter_event_type": "api.request",
    "currency": "USD"
  }'
# [Python]
resp = requests.post(
    "https://api.bill.sh/admin/v1/catalog/skus",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "plan_id": plan_id,
        "name": "API Requests",
        "pricing_model": "PerUnit",
        "unit_amount_nanos": "1000000",   # $0.000001 per request
        "meter_event_type": "api.request",
        "currency": "USD",
    },
)
sku = resp.json()
print(f"SKU {sku['id']} — {sku['pricing_model']} at {sku['unit_amount_nanos']} nanos/unit")
// [Node.js]
const resp = await fetch("https://api.bill.sh/admin/v1/catalog/skus", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    plan_id: planId,
    name: "API Requests",
    pricing_model: "PerUnit",
    unit_amount_nanos: "1000000",  // $0.000001 per request
    meter_event_type: "api.request",
    currency: "USD",
  }),
});
const sku = await resp.json();
console.log(`SKU ${sku.id} — ${sku.pricing_model}`);
// [Go]
body, _ := json.Marshal(map[string]interface{}{
    "plan_id":           planID,
    "name":              "API Requests",
    "pricing_model":     "PerUnit",
    "unit_amount_nanos": "1000000",
    "meter_event_type":  "api.request",
    "currency":          "USD",
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/catalog/skus",
    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 sku map[string]interface{}
json.NewDecoder(resp.Body).Decode(&sku)
fmt.Println("SKU:", sku["id"])

Note: unit_amount_nanos is the price in pico-units. 1000000 = $0.000001 per request.

Graduated Pricing Example

{
  "pricing_model": "Graduated",
  "tiers": [
    { "up_to": 1000000, "unit_amount_nanos": "1000000" },
    { "up_to": 10000000, "unit_amount_nanos": "750000" },
    { "up_to": null, "unit_amount_nanos": "500000" }
  ]
}

First 1M requests at $0.000001, next 9M at $0.00000075, everything after at $0.0000005.