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
| Contract | Subscription | |
|---|---|---|
| Owner | Sales / Legal | Finance / Engineering |
| Purpose | Commercial terms | Billing cycle execution |
| Lifecycle | Draft → Active → Amended/Expired/Terminated | Trialing → Active → Cancelled |
| PLG customers | None required | Required |
| Enterprise customers | Required | Linked via ContractSubscription |
Contract States
Draft → Active → Amended (immutable chain)
→ Expired (natural end)
→ Terminated (early exit)
Amendment is immutable — amend() 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.