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

SDK & Client Libraries

No official SDKs yet — but the OpenAPI spec makes generating one straightforward.

Generate a Client

Python (openapi-generator)

pip install openapi-generator-cli
openapi-generator generate \
  -i https://api.bill.sh/openapi.json \
  -g python \
  -o ./billing-client-python \
  --additional-properties=packageName=billing_client

TypeScript/Node.js

npx @openapitools/openapi-generator-cli generate \
  -i https://api.bill.sh/openapi.json \
  -g typescript-fetch \
  -o ./billing-client-ts

Go

openapi-generator generate \
  -i https://api.bill.sh/openapi.json \
  -g go \
  -o ./billing-client-go \
  --additional-properties=packageName=billingclient

Manual HTTP Client Pattern

Until official SDKs ship, here’s a minimal Python client that wraps requests with authentication, error handling, and idempotency support:

"""
billing_client.py — Minimal billing platform HTTP client
"""
from __future__ import annotations

import uuid
import hashlib
import requests
from typing import Any


class BillingClient:
    """Minimal client for the billing platform REST API."""

    def __init__(self, admin_token: str, base_url: str = "https://api.bill.sh"):
        self.base_url = base_url.rstrip("/")
        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {admin_token}",
            "Content-Type": "application/json",
        })

    def _idempotency_key(self, method: str, path: str, body: dict) -> str:
        """Generate a deterministic idempotency key from the request."""
        canonical = f"{method}:{path}:{sorted(body.items())}"
        return hashlib.sha256(canonical.encode()).hexdigest()[:32]

    def get(self, path: str, params: dict | None = None) -> Any:
        resp = self._session.get(f"{self.base_url}{path}", params=params)
        resp.raise_for_status()
        return resp.json()

    def post(self, path: str, body: dict, idempotency_key: str | None = None) -> Any:
        key = idempotency_key or self._idempotency_key("POST", path, body)
        headers = {"Idempotency-Key": key}
        resp = self._session.post(f"{self.base_url}{path}", json=body, headers=headers)
        resp.raise_for_status()
        return resp.json()

    def delete(self, path: str) -> None:
        resp = self._session.delete(f"{self.base_url}{path}")
        resp.raise_for_status()

    # --- Convenience methods ---

    def create_customer(self, display_name: str, email: str,
                        currency: str = "USD", **kwargs) -> dict:
        return self.post("/admin/v1/customers", {
            "display_name": display_name,
            "email": email,
            "currency": currency,
            **kwargs,
        })

    def create_subscription(self, customer_id: str, plan_id: str,
                            currency: str = "USD", **kwargs) -> dict:
        return self.post("/v1/subscriptions", {
            "customer_id": customer_id,
            "plan_id": plan_id,
            "currency": currency,
            **kwargs,
        }, idempotency_key=f"sub-{customer_id}-{plan_id}")

    def send_event(self, event_type: str, customer_id: str,
                   subscription_id: str, properties: dict,
                   timestamp: str | None = None) -> dict:
        import datetime
        event_id = f"evt-{uuid.uuid4()}"
        return self.post("/v1/events", {
            "id": event_id,
            "event_type": event_type,
            "customer_id": customer_id,
            "subscription_id": subscription_id,
            "timestamp": timestamp or datetime.datetime.utcnow().isoformat() + "Z",
            "properties": properties,
        }, idempotency_key=event_id)

    def get_usage(self, subscription_id: str) -> dict:
        return self.get(f"/v1/subscriptions/{subscription_id}/usage")

    def bill_subscription(self, subscription_id: str) -> None:
        self.post(f"/admin/v1/subscriptions/{subscription_id}/bill", {},
                  idempotency_key=f"bill-{subscription_id}")

    def finalize_invoice(self, invoice_id: str) -> dict:
        return self.post(f"/admin/v1/invoices/{invoice_id}/finalize", {},
                         idempotency_key=f"finalize-{invoice_id}")


# Usage example
if __name__ == "__main__":
    client = BillingClient(admin_token="bsk_live_your_key_here")

    # Create a customer and subscribe them
    customer = client.create_customer("Acme Corp", "billing@acme.com")
    subscription = client.create_subscription(
        customer_id=customer["id"],
        plan_id="your-plan-id",
        trial_days=14,
    )
    print(f"Customer {customer['id']} subscribed: {subscription['id']}")

    # Send a usage event
    result = client.send_event(
        event_type="api.request",
        customer_id=customer["id"],
        subscription_id=subscription["id"],
        properties={"model": "gpt-4o", "input_tokens": 512, "output_tokens": 128},
    )
    print(f"Event accepted: {result['event_id']}")

TypeScript Minimal Client

/**
 * billing-client.ts — Minimal TypeScript client
 */
export class BillingClient {
  private baseUrl: string;
  private token: string;

  constructor(token: string, baseUrl = "https://api.bill.sh") {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  private headers(extra: Record<string, string> = {}): HeadersInit {
    return {
      Authorization: `Bearer ${this.token}`,
      "Content-Type": "application/json",
      ...extra,
    };
  }

  async get<T>(path: string, params?: Record<string, string>): Promise<T> {
    const url = new URL(`${this.baseUrl}${path}`);
    if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    const resp = await fetch(url.toString(), { headers: this.headers() });
    if (!resp.ok) throw new Error(`GET ${path} failed: ${resp.status} ${await resp.text()}`);
    return resp.json() as Promise<T>;
  }

  async post<T>(path: string, body: object, idempotencyKey?: string): Promise<T> {
    const extra = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {};
    const resp = await fetch(`${this.baseUrl}${path}`, {
      method: "POST",
      headers: this.headers(extra),
      body: JSON.stringify(body),
    });
    if (!resp.ok) throw new Error(`POST ${path} failed: ${resp.status} ${await resp.text()}`);
    return resp.json() as Promise<T>;
  }

  async createCustomer(displayName: string, email: string, currency = "USD") {
    return this.post<{ id: string }>("/admin/v1/customers", {
      display_name: displayName, email, currency, account_type: "Organization",
    });
  }

  async subscribe(customerId: string, planId: string, opts?: { trial_days?: number }) {
    return this.post<{ id: string; status: string }>("/v1/subscriptions", {
      customer_id: customerId, plan_id: planId, currency: "USD", ...opts,
    }, `sub-${customerId}-${planId}`);
  }

  async sendEvent(customerId: string, subscriptionId: string,
                  eventType: string, properties: Record<string, unknown>) {
    const id = `evt-${crypto.randomUUID()}`;
    return this.post<{ accepted: boolean; event_id: string }>("/v1/events", {
      id, event_type: eventType, customer_id: customerId,
      subscription_id: subscriptionId,
      timestamp: new Date().toISOString(), properties,
    }, id);
  }
}

What’s in the OpenAPI Spec

The spec at https://api.bill.sh/openapi.json includes:

  • All request/response schemas with field descriptions
  • Auth requirements per endpoint
  • Idempotency header documentation
  • Error response formats

The Interactive API Explorer is powered by the same spec.