Why Automate Azure Billing?
For MSPs managing multiple clients, manual billing processes don't scale. Automation enables:
- Efficiency: Eliminate repetitive manual tasks
- Accuracy: Reduce human error in billing calculations
- Scalability: Manage 50+ clients without proportional staff increase
- Insights: Real-time cost monitoring and anomaly detection
- Integration: Connect Azure billing to PSA/accounting systems
- Client Value: Deliver real-time cost visibility and reporting
Azure Billing APIs Overview
Microsoft provides several APIs for programmatic access to billing and cost data.
Cost Management API
The primary API for querying cost and usage data.
Base URL: https://management.azure.com
Authentication: Azure AD OAuth 2.0
Key Capabilities:
- Query costs by subscription, resource group, or tag
- Filter by date range, service, location
- Aggregate data by day, week, or month
- Export usage details
- Retrieve budget and forecast data
Use Cases:
- Automated daily cost reports
- Client billing integration
- Cost anomaly detection
- Custom dashboards
Consumption API
Legacy API for usage details (being superseded by Cost Management API).
Key Endpoints:
- Usage Details
- Marketplace Charges
- Balances and Summaries
- Reserved Instance Recommendations
Note: Microsoft recommends Cost Management API for new implementations.
Billing API
Access to invoices, payment methods, and billing profiles.
Key Capabilities:
- Download invoices programmatically
- Retrieve billing periods
- Access payment status
- Manage billing profiles
Use Cases:
- Automated invoice retrieval
- Payment reconciliation
- Billing profile management
Setting Up API Access
Prerequisites
- Azure Subscription with resources to monitor
- Azure AD Application (service principal) for authentication
- Appropriate Permissions (Cost Management Reader minimum)
Step-by-Step Setup
Step 1: Register Azure AD Application
Azure Portal:
- Navigate to Azure Active Directory
- App Registrations → New Registration
- Name: "Cost Management API App"
- Supported Account Types: Single Tenant
- Click Register
Note: Save the Application (client) ID and Directory (tenant) ID
Step 2: Create Client Secret
- In your App Registration → Certificates & Secrets
- New Client Secret
- Description: "Cost API Secret"
- Expiration: 24 months (maximum)
- Click Add
IMPORTANT: Copy the secret value immediately. It's only shown once.
Step 3: Assign Permissions
Subscription Level:
- Navigate to Subscription → Access Control (IAM)
- Add Role Assignment
- Role: Cost Management Reader
- Assign access to: User, group, or service principal
- Select your app registration
- Save
For Multiple Subscriptions: Repeat for each subscription or assign at Management Group level.
Step 4: Test Authentication
PowerShell Example:
$tenantId = "your-tenant-id"
$clientId = "your-client-id"
$clientSecret = "your-client-secret"
$body = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
resource = "https://management.azure.com/"
}
$tokenResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/token" -Body $body
$accessToken = $tokenResponse.access_token
Write-Output "Successfully obtained access token"
Python Example:
from azure.identity import ClientSecretCredential
from azure.mgmt.costmanagement import CostManagementClient
credential = ClientSecretCredential(
tenant_id="your-tenant-id",
client_id="your-client-id",
client_secret="your-client-secret"
)
cost_mgmt_client = CostManagementClient(credential)
print("Successfully authenticated")
Querying Cost Data
Basic Cost Query
REST API Example (using PowerShell):
$subscriptionId = "your-subscription-id"
$scope = "/subscriptions/$subscriptionId"
$queryBody = @{
type = "Usage"
timeframe = "MonthToDate"
dataset = @{
granularity = "Daily"
aggregation = @{
totalCost = @{
name = "PreTaxCost"
function = "Sum"
}
}
}
} | ConvertTo-Json -Depth 10
$uri = "https://management.azure.com$scope/providers/Microsoft.CostManagement/query?api-version=2023-03-01"
$headers = @{
Authorization = "Bearer $accessToken"
"Content-Type" = "application/json"
}
$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $queryBody
$response.properties.rows
Response Format:
{
"properties": {
"rows": [
[125.67, 20251001, "USD"],
[134.23, 20251002, "USD"],
[142.89, 20251003, "USD"]
],
"columns": [
{"name": "PreTaxCost", "type": "Number"},
{"name": "UsageDate", "type": "Number"},
{"name": "Currency", "type": "String"}
]
}
}
Advanced Queries
Group by Service:
{
"type": "Usage",
"timeframe": "MonthToDate",
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ServiceName"
}
]
}
}
Filter by Tags:
{
"type": "Usage",
"timeframe": "MonthToDate",
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"filter": {
"tags": {
"name": "CostCenter",
"operator": "In",
"values": ["ClientA", "ClientB"]
}
}
}
}
Custom Date Range:
{
"type": "Usage",
"timePeriod": {
"from": "2025-09-01T00:00:00Z",
"to": "2025-09-30T23:59:59Z"
},
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
}
}
}
Automation Scenarios
Scenario 1: Daily Cost Report Email
Objective: Send email to MSP team with yesterday's Azure costs by client.
Implementation (Python + Azure Functions):
import os
import requests
from datetime import datetime, timedelta
from azure.identity import DefaultAzureCredential
from azure.mgmt.costmanagement import CostManagementClient
import smtplib
from email.mime.text import MIMEText
def get_daily_costs(subscription_id):
credential = DefaultAzureCredential()
client = CostManagementClient(credential)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
scope = f"/subscriptions/{subscription_id}"
query = {
"type": "Usage",
"timePeriod": {
"from": f"{yesterday}T00:00:00Z",
"to": f"{yesterday}T23:59:59Z"
},
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
}
}
}
result = client.query.usage(scope, query)
return result.rows[0][0] if result.rows else 0
def send_email(cost_data):
msg = MIMEText(f"Yesterday's Azure costs: ${cost_data:.2f}")
msg['Subject'] = f"Azure Daily Cost Report - {datetime.now().strftime('%Y-%m-%d')}"
msg['From'] = "billing@yourmsp.com"
msg['To'] = "team@yourmsp.com"
with smtplib.SMTP('smtp.office365.com', 587) as server:
server.starttls()
server.login("billing@yourmsp.com", os.getenv("EMAIL_PASSWORD"))
server.send_message(msg)
# Azure Function trigger
def main(mytimer):
subscriptions = ["sub-1", "sub-2", "sub-3"] # Your client subscriptions
total = 0
for sub_id in subscriptions:
cost = get_daily_costs(sub_id)
total += cost
send_email(total)
Deployment: Deploy as Azure Function with daily timer trigger (cron: 0 0 8 * * * for 8 AM daily).
Scenario 2: Cost Anomaly Detection
Objective: Alert when any client's daily cost exceeds 150% of their 30-day average.
Implementation (PowerShell):
function Get-AverageDailyCost {
param($subscriptionId, $days)
$endDate = Get-Date
$startDate = $endDate.AddDays(-$days)
# Query API for past $days costs
$query = @{
type = "Usage"
timePeriod = @{
from = $startDate.ToString("yyyy-MM-ddT00:00:00Z")
to = $endDate.ToString("yyyy-MM-ddT23:59:59Z")
}
dataset = @{
granularity = "Daily"
aggregation = @{
totalCost = @{
name = "PreTaxCost"
function = "Sum"
}
}
}
}
$result = Invoke-CostManagementQuery -SubscriptionId $subscriptionId -Query $query
$average = ($result.rows | Measure-Object -Property {$_[0]} -Average).Average
return $average
}
function Check-CostAnomaly {
param($subscriptionId, $clientName)
$average = Get-AverageDailyCost -subscriptionId $subscriptionId -days 30
$yesterday = Get-AverageDailyCost -subscriptionId $subscriptionId -days 1
$threshold = $average * 1.5
if ($yesterday -gt $threshold) {
$message = "ALERT: $clientName spent $$yesterday yesterday, exceeding 30-day average of $$average by 50%+"
Send-SlackAlert -Message $message
Send-EmailAlert -ClientName $clientName -Amount $yesterday
}
}
# Run for all clients
$clients = @(
@{Name="Client A"; SubId="sub-id-1"},
@{Name="Client B"; SubId="sub-id-2"}
)
foreach ($client in $clients) {
Check-CostAnomaly -subscriptionId $client.SubId -clientName $client.Name
}
Scenario 3: Client Cost Dashboard
Objective: Build custom dashboard showing real-time costs for all clients.
Tech Stack: Python (Flask) + Chart.js
Backend (Flask API):
from flask import Flask, jsonify
from azure.identity import DefaultAzureCredential
from azure.mgmt.costmanagement import CostManagementClient
app = Flask(__name__)
@app.route('/api/client-costs')
def get_client_costs():
credential = DefaultAzureCredential()
client = CostManagementClient(credential)
clients = [
{"name": "Client A", "subscription": "sub-1"},
{"name": "Client B", "subscription": "sub-2"}
]
results = []
for client_info in clients:
scope = f"/subscriptions/{client_info['subscription']}"
query = {
"type": "Usage",
"timeframe": "MonthToDate",
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {"name": "PreTaxCost", "function": "Sum"}
}
}
}
result = client.query.usage(scope, query)
cost = result.rows[0][0] if result.rows else 0
results.append({
"client": client_info['name'],
"cost": cost
})
return jsonify(results)
if __name__ == '__main__':
app.run()
Frontend (HTML + Chart.js):
<!DOCTYPE html>
<html>
<head>
<title>Client Cost Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<canvas id="costChart"></canvas>
<script>
fetch('/api/client-costs')
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('costChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(d => d.client),
datasets: [{
label: 'Month-to-Date Cost ($)',
data: data.map(d => d.cost),
backgroundColor: 'rgba(54, 162, 235, 0.5)'
}]
}
});
});
</script>
</body>
</html>
Scenario 4: Automated Invoice Download
Objective: Automatically download Microsoft CSP invoices when available.
Implementation (PowerShell + Partner Center API):
# Partner Center authentication
$credential = Get-Credential
Connect-PartnerCenter -Credential $credential
# Get latest invoice
$invoices = Get-PartnerInvoice -BillingPeriod Current
$latestInvoice = $invoices | Sort-Object InvoiceDate -Descending | Select-Object -First 1
# Download invoice PDF
$invoicePdf = Get-PartnerInvoiceStatement -InvoiceId $latestInvoice.InvoiceId
$invoicePdf | Out-File -FilePath "C:\Invoices\$($latestInvoice.InvoiceId).pdf"
# Download reconciliation file
$recon = Get-PartnerInvoiceLineItem -InvoiceId $latestInvoice.InvoiceId -BillingProvider Azure
$recon | Export-Csv -Path "C:\Invoices\$($latestInvoice.InvoiceId)_recon.csv"
# Email to accounting team
Send-MailMessage `
-To "accounting@yourmsp.com" `
-From "billing@yourmsp.com" `
-Subject "Azure Invoice Available: $($latestInvoice.InvoiceId)" `
-Body "New invoice and reconciliation file attached" `
-Attachments "C:\Invoices\$($latestInvoice.InvoiceId).pdf", "C:\Invoices\$($latestInvoice.InvoiceId)_recon.csv" `
-SmtpServer "smtp.office365.com" `
-UseSSL
Automation: Schedule as daily task starting on 6th of each month.
Integration with Business Systems
PSA Tool Integration (e.g., ConnectWise, Autotask)
Objective: Automatically create invoices in PSA system based on Azure costs.
High-Level Process:
- Query Azure Cost Management API for monthly costs by client
- Map Azure subscription IDs to PSA customer IDs
- Create invoice line items in PSA system
- Apply markup percentage
- Generate invoice
Example (ConnectWise Manage API):
import requests
from azure.mgmt.costmanagement import CostManagementClient
from azure.identity import DefaultAzureCredential
def create_cw_invoice(customer_id, azure_cost, markup=0.25):
cw_api_url = "https://api-na.myconnectwise.net/v2019_4/apis/3.0"
auth = ("company+publickey", "privatekey")
total = azure_cost * (1 + markup)
invoice = {
"companyId": customer_id,
"invoiceLines": [
{
"description": "Azure Cloud Services - October 2025",
"quantity": 1,
"unitPrice": total,
"amount": total
}
]
}
response = requests.post(
f"{cw_api_url}/finance/invoices",
auth=auth,
json=invoice
)
return response.json()
# Get Azure costs
credential = DefaultAzureCredential()
cost_client = CostManagementClient(credential)
subscriptions = {
"sub-id-1": {"name": "Client A", "cw_id": 123},
"sub-id-2": {"name": "Client B", "cw_id": 456}
}
for sub_id, info in subscriptions.items():
# Query Azure cost for subscription
scope = f"/subscriptions/{sub_id}"
query = {
"type": "Usage",
"timeframe": "Custom",
"timePeriod": {
"from": "2025-10-01T00:00:00Z",
"to": "2025-10-31T23:59:59Z"
},
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {"name": "PreTaxCost", "function": "Sum"}
}
}
}
result = cost_client.query.usage(scope, query)
azure_cost = result.rows[0][0]
# Create ConnectWise invoice
create_cw_invoice(info['cw_id'], azure_cost)
print(f"Created invoice for {info['name']}: ${azure_cost:.2f}")
Accounting System Integration (e.g., QuickBooks)
Similar approach using QuickBooks Online API to create invoices programmatically.
Best Practices for Automation
Security
Protect API Credentials:
- Store client secrets in Azure Key Vault
- Use Managed Identities where possible (Azure Functions, VMs)
- Rotate secrets annually
- Never commit secrets to source control
Least Privilege Access:
- Cost Management Reader for read-only API access
- Avoid Owner or Contributor unless necessary
- Separate service principals per automation task
Reliability
Error Handling:
- Implement retry logic (exponential backoff)
- Handle API throttling (429 responses)
- Log all errors for troubleshooting
- Alert on repeated failures
Monitoring:
- Use Application Insights for Azure Functions
- Track automation success/failure rates
- Alert when automations don't run
- Monitor API quota usage
Testing
Develop in Non-Production:
- Test against dev/test subscriptions first
- Validate query results manually before automation
- Use small datasets initially
Gradual Rollout:
- Start with one client subscription
- Expand to 5-10 once stable
- Full rollout after 1 month of reliable operation
Documentation
Maintain Runbooks:
- Document what each automation does
- Include troubleshooting steps
- Note dependencies and prerequisites
- Keep credentials inventory (where stored, rotation schedule)
Code Comments:
- Explain complex query logic
- Document why specific parameters chosen
- Include example API responses
Common Automation Pitfalls
Pitfall 1: Hardcoding Subscription IDs
- Solution: Store in config file or database
- Makes adding/removing clients easier
Pitfall 2: Not Handling API Throttling
- Azure APIs have rate limits
- Solution: Implement exponential backoff retry logic
Pitfall 3: Ignoring Time Zones
- Azure uses UTC timestamps
- Solution: Always use UTC in queries, convert for display
Pitfall 4: Over-Engineering
- Don't build a complex system on day one
- Start simple, iterate based on needs
Pitfall 5: Not Monitoring Automation
- Automations fail silently
- Solution: Alerts when automation doesn't run or errors occur
Tools and Libraries
Microsoft Official SDKs
Python:
azure-mgmt-costmanagementazure-identityazure-mgmt-billing
PowerShell:
Az.CostManagementAz.BillingAz.Accounts
.NET:
Azure.ResourceManager.CostManagementAzure.Identity
Third-Party Tools
Terraform: Automate Azure Policy for cost management
Ansible: Configuration management for cost controls
Jenkins/GitHub Actions: CI/CD pipelines for automation deployment
Next Steps
- Implement automated cost monitoring with Azure Cost Management Tools
- Use Azure Tagging Strategies to improve cost attribution
- Review MSP Billing Best Practices for billing workflow integration
Automation transforms billing from time-consuming chore to strategic asset. Start small, measure value, scale systematically.