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"])