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

Contracts

Contracts represent commercial/legal agreements between your company and an enterprise customer. They live in the contracts layer — separate from subscriptions (which are billing engine artifacts) — because Sales owns contracts while Finance/Engineering owns subscriptions.

Contract vs Subscription

ContractSubscription
OwnerSales / LegalFinance / Engineering
PurposeCommercial termsBilling cycle execution
LifecycleDraft → Active → Amended/Expired/TerminatedTrialing → Active → Cancelled
PLG customersNone requiredRequired
Enterprise customersRequiredLinked via ContractSubscription

Contract States

Draft → Active → Amended (immutable chain)
              → Expired (natural end)
              → Terminated (early exit)

Amendment is immutableamend() marks the current contract Amended and creates a new Active contract with parent_contract_id linking back. The full history is queryable via the amendment chain.

Key Concepts

Ramp Contracts

Enterprise contracts often have ramp phases — lower pricing for the first months, ramping up to full rate. Each ContractTerm phase has its own pricing, discount, and committed amount.

Commit Draw-Down

When a customer has a committed minimum spend (e.g., $100k/year), usage charges first draw down from the commit. apply_commit_drawdown() applies draws in priority order, tag-scoped if needed.

Coterming

When a customer adds a new subscription mid-contract, it can be cotermed to the existing contract’s end date via coterm(). If fewer than 30 days remain, a one-time fee is generated instead.

Escalation

Contracts support automatic price escalation: Percent (e.g., 3% annual) or Fixed amount per period.

Rate Overrides

Enterprise custom pricing via RateOverride — a basis-points multiplier scoped to a product_tag. Lets you give a customer 20% off all storage SKUs without touching the catalog.

Contract CRUD

Create a Contract

# [curl]
curl -X POST https://api.bill.sh/admin/v1/contracts \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "01944b1f-0000-7000-8000-000000000001",
    "name": "Acme Corp — Enterprise 2026",
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
    "terms": [
      {
        "phase": 1,
        "start_date": "2026-01-01",
        "end_date": "2026-03-31",
        "committed_amount_nanos": "50000000000000000",
        "description": "Q1 ramp — $50k"
      },
      {
        "phase": 2,
        "start_date": "2026-04-01",
        "end_date": "2026-12-31",
        "committed_amount_nanos": "100000000000000000",
        "description": "Q2-Q4 full rate — $100k/quarter"
      }
    ]
  }'
# [Python]
import requests

resp = requests.post(
    "https://api.bill.sh/admin/v1/contracts",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "customer_id": customer_id,
        "name": "Acme Corp — Enterprise 2026",
        "start_date": "2026-01-01",
        "end_date": "2026-12-31",
        "terms": [
            {
                "phase": 1,
                "start_date": "2026-01-01",
                "end_date": "2026-03-31",
                "committed_amount_nanos": "50000000000000000",
                "description": "Q1 ramp — $50k",
            },
            {
                "phase": 2,
                "start_date": "2026-04-01",
                "end_date": "2026-12-31",
                "committed_amount_nanos": "100000000000000000",
                "description": "Q2-Q4 full rate — $100k/quarter",
            },
        ],
    },
)
contract = resp.json()
print(f"Contract {contract['id']} — {contract['status']}")
// [Node.js]
const resp = await fetch("https://api.bill.sh/admin/v1/contracts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    customer_id: customerId,
    name: "Acme Corp — Enterprise 2026",
    start_date: "2026-01-01",
    end_date: "2026-12-31",
    terms: [
      {
        phase: 1,
        start_date: "2026-01-01",
        end_date: "2026-03-31",
        committed_amount_nanos: "50000000000000000",
        description: "Q1 ramp — $50k",
      },
      {
        phase: 2,
        start_date: "2026-04-01",
        end_date: "2026-12-31",
        committed_amount_nanos: "100000000000000000",
        description: "Q2-Q4 full rate — $100k/quarter",
      },
    ],
  }),
});
const contract = await resp.json();
console.log(`Contract ${contract.id} — ${contract.status}`);
// [Go]
type ContractTerm struct {
    Phase                 int    `json:"phase"`
    StartDate             string `json:"start_date"`
    EndDate               string `json:"end_date"`
    CommittedAmountNanos  string `json:"committed_amount_nanos"`
    Description           string `json:"description"`
}

payload := map[string]interface{}{
    "customer_id": customerID,
    "name":        "Acme Corp — Enterprise 2026",
    "start_date":  "2026-01-01",
    "end_date":    "2026-12-31",
    "terms": []ContractTerm{
        {1, "2026-01-01", "2026-03-31", "50000000000000000", "Q1 ramp — $50k"},
        {2, "2026-04-01", "2026-12-31", "100000000000000000", "Q2-Q4 full rate"},
    },
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/contracts",
    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 contract map[string]interface{}
json.NewDecoder(resp.Body).Decode(&contract)
fmt.Printf("Contract %v — %v\n", contract["id"], contract["status"])

Get a Contract

# [curl]
curl https://api.bill.sh/admin/v1/contracts/$CONTRACT_ID \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
resp = requests.get(
    f"https://api.bill.sh/admin/v1/contracts/{contract_id}",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
contract = resp.json()
print(f"Contract: {contract['name']}, Status: {contract['status']}")
print(f"Terms: {len(contract.get('terms', []))}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/contracts/${contractId}`,
  { headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` } }
);
const contract = await resp.json();
console.log(`Contract: ${contract.name}, Status: ${contract.status}`);
// [Go]
req, _ := http.NewRequest("GET",
    "https://api.bill.sh/admin/v1/contracts/"+contractID, nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var contract map[string]interface{}
json.NewDecoder(resp.Body).Decode(&contract)

Amend a Contract

# [curl]
curl -X POST https://api.bill.sh/admin/v1/contracts/$CONTRACT_ID/amend \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Expansion — added Data Analytics module",
    "new_committed_amount_nanos": "150000000000000000",
    "effective_date": "2026-06-01"
  }'
# [Python]
resp = requests.post(
    f"https://api.bill.sh/admin/v1/contracts/{contract_id}/amend",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "reason": "Expansion — added Data Analytics module",
        "new_committed_amount_nanos": "150000000000000000",
        "effective_date": "2026-06-01",
    },
)
amended = resp.json()
print(f"New contract: {amended['id']}, parent: {amended.get('parent_contract_id')}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/contracts/${contractId}/amend`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${ADMIN_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      reason: "Expansion — added Data Analytics module",
      new_committed_amount_nanos: "150000000000000000",
      effective_date: "2026-06-01",
    }),
  }
);
const amended = await resp.json();
console.log(`New contract: ${amended.id}, parent: ${amended.parent_contract_id}`);
// [Go]
body, _ := json.Marshal(map[string]string{
    "reason":                      "Expansion — added Data Analytics module",
    "new_committed_amount_nanos":  "150000000000000000",
    "effective_date":              "2026-06-01",
})
req, _ := http.NewRequest("POST",
    "https://api.bill.sh/admin/v1/contracts/"+contractID+"/amend",
    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()

Contract Lifecycle (Customer 360)

# [curl]
# The contract API is managed internally by the Sales/Finance team.
# External integrations access contract data via the Customer 360 view.

curl https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# Returns linked contracts under the customer's subscriptions
# [Python]
resp = requests.get(
    f"https://api.bill.sh/admin/v1/customers/{customer_id}",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
view = resp.json()
# Access linked contracts
for sub in view.get("subscriptions", []):
    if sub.get("contract_id"):
        print(f"Subscription {sub['id']} linked to contract {sub['contract_id']}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/customers/${customerId}`,
  { headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` } }
);
const view = await resp.json();
for (const sub of view.subscriptions ?? []) {
  if (sub.contract_id) {
    console.log(`Subscription ${sub.id} linked to contract ${sub.contract_id}`);
  }
}
// [Go]
req, _ := http.NewRequest("GET",
    "https://api.bill.sh/admin/v1/customers/"+customerID, nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var view map[string]interface{}
json.NewDecoder(resp.Body).Decode(&view)

See the Interactive API Explorer for full contract CRUD endpoints.