Billing Integration
AMS uses a pluggable billing service architecture that allows integration with various billing providers. Currently, Xero is the supported billing provider using the Custom Connection flow.
Overview
The billing system handles:
- Creating and managing customer contacts
- Generating invoices for memberships and services
- Sending invoice emails
- Tracking invoice payment status via webhooks
- Synchronizing invoice data between AMS and the billing provider
Xero Integration
Custom Connection Flow
AMS uses Xero's Custom Connection authentication flow, which is designed for single-organization integrations. This approach:
- Uses OAuth2 client credentials grant type
- Connects to a single pre-authorized Xero organization
- Requires no interactive user authentication
- Is ideal for backend integrations like AMS
Important: Before configuring AMS, you must set up a Custom Connection in the Xero Developer Portal following the steps below.
Setting Up a Xero Custom Connection
-
Create the Custom Connection in Xero My Apps:
- Click "New App"
- Give it a name (e.g., "AMS Billing Integration")
- Select "Custom connection" as the integration type
-
Select Required Scopes:
- ✅
accounting.contacts- For creating and managing customer contacts - ✅
accounting.transactions- For creating, retrieving, and managing invoices
- ✅
-
Select the Authorizing User:
- Choose the user who will authorize the connection
- They will receive an email with a link to authorize
-
Authorize the Connection:
- The selected user clicks the link in their email
- They consent to the requested scopes
- They select the Xero organization to connect
- Note: The organization must have purchased a Custom Connection subscription (the Xero Demo Company can be used for free during development)
-
Retrieve Credentials:
- Once authorized, return to the app details page
- Copy the Client ID
- Generate and copy the Client Secret (keep this secure!)
- Note your Tenant ID (organization ID) from the connection details
Configuration
Configure the following environment variables for Xero integration:
Required Variables
| Variable | Description | Example |
|---|---|---|
AMS_BILLING_SERVICE_CLASS |
The billing service provider class | ams.billing.providers.xero.XeroBillingService |
XERO_CLIENT_ID |
OAuth2 client ID from your Custom Connection | 91E5715B1199038080D6D0296EBC1648 |
XERO_CLIENT_SECRET |
OAuth2 client secret from your Custom Connection | your-secret-here |
XERO_TENANT_ID |
Xero organization/tenant ID | a3a4dbaf-3495-a808-ed7a-7b964388f53 |
XERO_WEBHOOK_KEY |
Webhook signing key for validating webhook requests | your-webhook-key |
XERO_ACCOUNT_CODE |
Default account code for invoice line items | 200 |
XERO_AMOUNT_TYPE |
Tax calculation type | INCLUSIVE or EXCLUSIVE |
XERO_CURRENCY_CODE |
Currency code for invoices | NZD, AUD, USD, etc. |
Optional Variables
| Variable | Description | Example | Default |
|---|---|---|---|
AMS_BILLING_EMAIL_WHITELIST_REGEX |
Regex pattern to filter invoice email recipients (for testing) | @example.com$ |
None |
XERO_EMAIL_INVOICES |
Enable sending invoice emails via Xero (set to False when using Xero Demo Company) |
True or False |
True |
XERO_DEBUG |
Enable HTTP request/response debugging for Xero API calls | True or False |
False |
Security Warning
Setting XERO_DEBUG=True will log all HTTP requests and responses, including sensitive credentials and bearer tokens. Only enable this for debugging specific API issues in isolated development environments. Never enable in production.
Deployment Configuration
When deploying AMS with Xero integration, configure these environment variables in your deployment platform.
Required Environment Variables
These variables must be set for production deployments:
# Billing Provider Selection
AMS_BILLING_SERVICE_CLASS=ams.billing.providers.xero.XeroBillingService
# Xero API Credentials (from Custom Connection)
XERO_CLIENT_ID=your-client-id-here
XERO_CLIENT_SECRET=your-client-secret-here
XERO_TENANT_ID=your-tenant-id-here
# Webhook Configuration
XERO_WEBHOOK_KEY=your-webhook-key-here
# Xero Organization Settings
XERO_ACCOUNT_CODE=200
XERO_AMOUNT_TYPE=INCLUSIVE
XERO_CURRENCY_CODE=NZD
Optional Environment Variables
# Email Configuration
XERO_EMAIL_INVOICES=True # Set to False for Xero Demo Company
AMS_BILLING_EMAIL_WHITELIST_REGEX= # Leave empty for production
# Debugging (NEVER enable in production)
XERO_DEBUG=False
Security Best Practices
- Never commit credentials to version control
- Use secret management provided by your platform
- Rotate credentials periodically (generate new client secrets in Xero)
- Restrict access to production credentials
- Use different Xero apps for development, staging, and production environments
Webhook Configuration
AMS receives webhook notifications from Xero when invoice status changes (e.g., paid, voided). The webhook endpoint uses HMAC-SHA256 signature verification:
Webhook Endpoint: https://your-domain.com/billing/xero/webhooks/
Setting Up Webhooks in Xero
- Navigate to your Xero app in the Developer Portal
- Go to the Webhooks section:
https://developer.xero.com/app/manage/app/YOUR_APP_ID/webhooks - Configure the webhook:
- Delivery URL:
https://your-domain.com/billing/xero/webhooks/ - Xero will generate a Webhook Key - save this as
XERO_WEBHOOK_KEY
- Delivery URL:
- Save the configuration
Webhook Events Handled
Currently, AMS processes these Xero webhook events:
{
"eventCategory": "INVOICE",
"eventType": "UPDATE",
"tenantId": "your-tenant-id",
"resourceId": "invoice-id-here"
}
Processing Flow:
- Webhook received → Signature verified
- Invoice UPDATE events extracted
- Matching invoices marked with
update_needed=True - Response sent (200 OK)
- After response,
fetch_updated_invoice_details()triggered - Invoice details fetched from Xero API
- Local database updated with latest payment status
Testing Webhooks
Local Development:
Use ngrok or similar tool:
ngrok http 8000
Configure in your .envs/.local/django-private.ini:
NGROK_HOST=your-subdomain.ngrok-free.dev
Update your Xero app webhook URL:
https://your-subdomain.ngrok-free.dev/billing/xero/webhooks/
Manual Testing:
Trigger webhook events by making changes in Xero:
- Mark an invoice as paid in Xero
- Check AMS logs for webhook receipt
- Verify invoice status updated in AMS admin
Architecture
Service Class Hierarchy
BillingService (ABC)
├── XeroBillingService
│ └── MockXeroBillingService (for testing)
└── MockBillingService (generic mock)
Models
Account:
class Account(Model):
organisation = OneToOneField(Organisation, ...)
user = OneToOneField(User, ...)
# Either organisation or user must be set
XeroContact:
class XeroContact(Model):
account = OneToOneField(Account, ...)
contact_id = CharField(max_length=255) # Xero's contact ID
Invoice:
class Invoice(Model):
account = ForeignKey(Account, ...)
invoice_number = CharField(max_length=255, unique=True)
billing_service_invoice_id = CharField(...) # Xero's invoice ID
update_needed = BooleanField(default=False)
# Amount fields, dates, etc.
Key Service Methods
Contact Management:
def update_user_billing_details(user: User) -> None:
"""Create or update Xero contact for a user."""
def update_organisation_billing_details(organisation: Organisation) -> None:
"""Create or update Xero contact for an organisation."""
Invoice Management:
def create_invoice(
account: Account,
date: date,
due_date: date,
line_items: list[dict[str, Any]],
reference: str,
) -> Invoice:
"""Create invoice in Xero and local DB."""
def email_invoice(invoice: Invoice) -> None:
"""Send invoice email via Xero."""
def update_invoices(billing_service_invoice_ids: list[str]) -> None:
"""Fetch latest invoice data from Xero."""
def get_invoice_url(invoice: Invoice) -> str | None:
"""Get customer-facing online invoice URL."""
Rate Limiting
Xero enforces API rate limits to prevent abuse and ensure service stability. Understanding and handling these limits is crucial for reliable operation.
Xero Rate Limit Details
- Rate Limit: 60 requests per minute per organization
- Limit Window: Rolling 60-second window
- Headers: Xero returns rate limit information in response headers:
X-Rate-Limit-Limit: Maximum requests allowed (60)X-Rate-Limit-Remaining: Requests remaining in current windowX-Rate-Limit-Problem: Returned when limit is exceeded
AMS Rate Limit Handling
The integration uses a fail-fast approach with the @handle_rate_limit() decorator:
from ams.billing.providers.xero.rate_limiting import handle_rate_limit
@handle_rate_limit()
def _create_xero_invoice(self, ...):
# API call here
pass
Behavior:
- API calls are wrapped with rate limit detection
- When rate limits are exceeded (HTTP 429),
XeroRateLimitErroris raised - The error includes
retry_afterseconds when available from Xero's response - No automatic retry - operations fail immediately to prevent cascading delays
Handling Rate Limit Errors
During Webhook Processing:
- Webhook handlers mark invoices as
update_needed=Trueinstead of fetching immediately - The
fetch_invoice_updatescommand processes updates in batches - Limits processing to 30 invoices per run to avoid hitting rate limits
During Bulk Operations:
If performing bulk operations (e.g., importing many members):
from ams.billing.providers.xero.rate_limiting import XeroRateLimitError
import time
for member in members:
try:
billing_service.update_user_billing_details(member)
except XeroRateLimitError as e:
# Wait for the retry_after period
time.sleep(e.retry_after or 60)
# Retry or defer to next run
continue
Recommended Strategies:
- Batch Processing: Process records in small batches with delays between batches
- Scheduled Jobs: Spread bulk operations across multiple cron runs
- Queue-based Processing: Use a task queue (e.g., Celery) with rate limiting
- Monitor Remaining Requests: Check
X-Rate-Limit-Remainingheader to throttle proactively
Rate Limit Monitoring
Log rate limit errors to track API usage patterns:
import logging
logger = logging.getLogger(__name__)
try:
billing_service.create_invoice(...)
except XeroRateLimitError as e:
logger.warning(
"Xero rate limit exceeded. Retry after %s seconds",
e.retry_after
)
Configure Sentry or your monitoring system to alert on rate limit errors for proactive response.
Testing
Unit Tests
Run billing tests:
pytest ams/billing/tests/
Key test files:
test_invoice_model.py- Invoice model teststest_account_model.py- Account model teststest_fetch_invoice_updates_command.py- Management command tests
Mock Service for Testing
For testing without connecting to Xero, use MockXeroBillingService:
# In config/settings/test.py
BILLING_SERVICE_CLASS = "ams.billing.providers.xero.MockXeroBillingService"
The mock service:
- Returns dummy data for all operations
- Does not make external API calls
- Useful for unit testing and CI/CD pipelines
- Creates predictable invoice IDs and numbers
Management Commands
fetch_invoice_updates
Manually fetch and update invoice details from Xero:
python manage.py fetch_invoice_updates
Purpose:
- Queries local invoices marked with
update_needed=True - Fetches latest data from Xero API (payment status, amounts, dates)
- Updates local database with current information
- Marks invoices as no longer needing updates
Behavior:
- Processes up to 30 invoices per run (to avoid rate limits)
- Only works with
XeroBillingService(skips mock services) - Logs progress and results to stdout
- Raises exceptions for debugging when called manually
When to Use:
- After webhook outages or delivery failures
- During initial data migration from Xero
- For manual invoice status verification
- In scheduled cron jobs to catch missed webhook events
Troubleshooting
Authentication Issues
Symptom: "Invalid credentials" or "Unauthorized" errors
Possible Causes:
- Incorrect
XERO_CLIENT_IDorXERO_CLIENT_SECRET - Custom Connection not authorized or authorization expired
- Incorrect
XERO_TENANT_ID
Solutions:
- Verify credentials in Xero Developer Portal match environment variables
- Check Custom Connection is still authorized (hasn't been revoked)
- Confirm
XERO_TENANT_IDmatches the connected organization - Try regenerating client secret and updating
XERO_CLIENT_SECRET
Webhook Verification Failures
Symptom: Webhooks return 401 Unauthorized
Possible Causes:
- Incorrect
XERO_WEBHOOK_KEY - Webhook key changed in Xero but not updated in AMS
- Request not actually from Xero (spoofing attempt)
Solutions:
- Verify
XERO_WEBHOOK_KEYmatches the key shown in Xero Developer Portal - Check Xero's webhook delivery logs for signature details
- Test webhook signature locally
Rate Limit Errors
Symptom: XeroRateLimitError raised during operations
Cause: Exceeded Xero's 60 requests per minute limit
Solutions:
- Immediate: Wait for the
retry_afterperiod before retrying - Short-term: Reduce concurrent operations or add delays between requests
- Long-term: Implement queueing system with rate limiting
Invoice Creation Failures
Symptom: Invoice creation fails or returns errors
Possible Causes:
- Account missing associated
XeroContact - Invalid
XERO_ACCOUNT_CODEfor the organization - Unsupported
XERO_CURRENCY_CODE - Missing
accounting.transactionsscope - Invalid line item data
Contact Creation or Update Failures
Symptom: Contact operations fail
Possible Causes:
- Missing
accounting.contactsscope - Duplicate contact name (shouldn't happen with UUID prefix)
- Invalid email address format
Invoice Status Not Updating
Symptom: Invoice paid in Xero but still shows as unpaid in AMS
Possible Causes:
- Webhook not configured or failing
fetch_invoice_updatescommand not runningupdate_neededflag not being set
Email Invoices Not Sending
Symptom: Invoices created but emails not received
Possible Causes:
XERO_EMAIL_INVOICES=Falsein settings- Using Xero Demo Company (emails disabled)
- Invalid contact email address
AMS_BILLING_EMAIL_WHITELIST_REGEXfiltering recipient
Getting Help
If issues persist:
- Check Xero API Status: status.xero.com
- Review Xero API Logs: Developer Portal → Your App → API Logs
- Enable Debug Logging: Set
XERO_DEBUG=True(development only) - Check Django Logs: Review application logs for detailed error messages
- Consult Xero Documentation: developer.xero.com
- Contact Support: Reach out to your AMS implementation team with:
- Error messages (sanitize any credentials)
- Steps to reproduce
- Django and Xero API logs
- Environment details (development/staging/production)