Azure Billing APIs & Automation

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

  1. Azure Subscription with resources to monitor
  2. Azure AD Application (service principal) for authentication
  3. Appropriate Permissions (Cost Management Reader minimum)

Step-by-Step Setup

Step 1: Register Azure AD Application

Azure Portal:

  1. Navigate to Azure Active Directory
  2. App Registrations → New Registration
  3. Name: "Cost Management API App"
  4. Supported Account Types: Single Tenant
  5. Click Register

Note: Save the Application (client) ID and Directory (tenant) ID

Step 2: Create Client Secret

  1. In your App Registration → Certificates & Secrets
  2. New Client Secret
  3. Description: "Cost API Secret"
  4. Expiration: 24 months (maximum)
  5. Click Add

IMPORTANT: Copy the secret value immediately. It's only shown once.

Step 3: Assign Permissions

Subscription Level:

  1. Navigate to Subscription → Access Control (IAM)
  2. Add Role Assignment
  3. Role: Cost Management Reader
  4. Assign access to: User, group, or service principal
  5. Select your app registration
  6. 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:

  1. Query Azure Cost Management API for monthly costs by client
  2. Map Azure subscription IDs to PSA customer IDs
  3. Create invoice line items in PSA system
  4. Apply markup percentage
  5. 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-costmanagement
  • azure-identity
  • azure-mgmt-billing

PowerShell:

  • Az.CostManagement
  • Az.Billing
  • Az.Accounts

.NET:

  • Azure.ResourceManager.CostManagement
  • Azure.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.