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

Tax Engine

The billing platform uses a pluggable TaxAdapter trait for tax calculation. This lets you start with zero-tax for development, use a simple flat-rate adapter for testing, and connect to a full tax engine (Avalara, TaxJar) in production — all without changing the billing core.

TaxAdapter Trait

#![allow(unused)]
fn main() {
pub trait TaxAdapter: Send + Sync {
    fn calculate_tax(
        &self,
        line_items: &[InvoiceLineItem],
        customer: &Customer,
    ) -> Result<Option<TaxResult>, BillingError>;
}
}

The adapter receives the invoice line items and customer context. It returns either None (no tax) or a TaxResult with the tax amount and description to add as a line item.

Built-in Adapters

PassThroughTaxAdapter (Default)

Always returns zero tax. Used in development and for B2B customers who handle their own tax reporting.

# Configured via AppState — default in all environments
tax_adapter: Arc::new(PassThroughTaxAdapter)

FlatRateTaxAdapter

Applies a flat percentage (specified in basis points) to all non-tax line items. Respects B2B exemption.

#![allow(unused)]
fn main() {
// 8.5% tax = 850 basis points
let adapter = FlatRateTaxAdapter::new(850);

// B2B customers with a tax_id are exempt
// The adapter checks customer.tax_id.is_some()
}

Production: Avalara / TaxJar

For production deployments, implement the TaxAdapter trait against your preferred tax engine’s API. The adapter receives customer.billing_address and customer.tax_id for jurisdiction and exemption determination.

Apply Tax to an Invoice

Tax is applied to a Draft invoice before finalization:

# [curl]
curl -X POST https://api.bill.sh/admin/v1/invoices/$INV_ID/calculate-tax \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
import requests

resp = requests.post(
    f"https://api.bill.sh/admin/v1/invoices/{inv_id}/calculate-tax",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
result = resp.json()
if result["tax_applied"]:
    tax_nanos = int(result["tax_line_item"]["amount_nanos"])
    tax_usd = tax_nanos / 1e12
    print(f"Tax applied: ${tax_usd:.2f} — {result['tax_line_item']['description']}")
else:
    print(f"No tax: {result['message']}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/invoices/${invId}/calculate-tax`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
  }
);
const result = await resp.json();
if (result.tax_applied) {
  const taxUsd = (BigInt(result.tax_line_item.amount_nanos) / BigInt(1e12)).toString();
  console.log(`Tax applied: $${taxUsd} — ${result.tax_line_item.description}`);
} else {
  console.log("No tax:", result.message);
}
// [Go]
req, _ := http.NewRequest("POST",
    "https://api.bill.sh/admin/v1/invoices/"+invID+"/calculate-tax", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["tax_applied"].(bool) {
    lineItem := result["tax_line_item"].(map[string]interface{})
    fmt.Printf("Tax applied: %v — %v\n", lineItem["amount_nanos"], lineItem["description"])
} else {
    fmt.Println("No tax:", result["message"])
}

Response (tax applied):

{
  "tax_applied": true,
  "tax_line_item": {
    "id": "01944b1f-0000-7000-8000-000000000050",
    "description": "Sales Tax (8.5%)",
    "amount_nanos": "849150000000",
    "currency": "USD",
    "is_tax": true
  }
}

Response (zero tax):

{
  "tax_applied": false,
  "message": "zero tax — no tax line item added"
}

Idempotency

This endpoint is idempotent. The adapter excludes existing tax line items (is_tax: true) from the taxable subtotal, so calling it twice does not compound tax. It’s safe to call multiple times before finalization.

B2B Tax Exemption

Customers with a tax_id set (VAT number, EIN, etc.) are treated as B2B and exempt from tax collection when using FlatRateTaxAdapter. The customer is responsible for reverse-charge VAT in their jurisdiction.

# [curl]
# Set tax_id on customer creation or update
curl -X POST https://api.bill.sh/admin/v1/customers \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "display_name": "Acme GmbH", "email": "billing@acme.de", "currency": "EUR", "tax_id": "DE123456789" }'
# [Python]
resp = requests.post(
    "https://api.bill.sh/admin/v1/customers",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "display_name": "Acme GmbH",
        "email": "billing@acme.de",
        "currency": "EUR",
        "tax_id": "DE123456789",   # B2B exempt from tax collection
    },
)
customer = resp.json()
print(f"Customer {customer['id']} — tax exempt with VAT: DE123456789")
// [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 GmbH",
    email: "billing@acme.de",
    currency: "EUR",
    tax_id: "DE123456789",  // B2B exempt from tax collection
  }),
});
const customer = await resp.json();
console.log(`Customer ${customer.id} — tax exempt`);
// [Go]
body, _ := json.Marshal(map[string]string{
    "display_name": "Acme GmbH",
    "email":        "billing@acme.de",
    "currency":     "EUR",
    "tax_id":       "DE123456789",
})
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)
fmt.Printf("Customer %v — tax exempt\n", customer["id"])