PAYATHON 2026

如何使用 Azure Billing API 获取订阅的总账单金额

支付老李

结论

不需要再通过 RateCard × Usage 手工计算。指定订阅和时间范围后,可以使用 Azure Cost Management Query API 对 PreTaxCost 求和,直接得到类似 500 USD 的汇总金额。

这里需要区分两类金额:

  • 订阅成本汇总:通过 Cost Management Query API 获取,适合展示当月累计成本或指定期间的成本。
  • 最终应付账单:通过 Azure Billing Invoices API 查询已结算的发票。发票通常归属于 billing account、billing profile 或 invoice section,不一定与单个 subscription 一一对应。

Microsoft.Commerce/UsageAggregates 和 RateCard 是较早的预览接口,主要用于获取用量和费率明细,不适合用于新的账单汇总实现。

查询订阅的成本总额

将请求范围设置为 subscription,并且只配置一项求和聚合:

POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version={supported-api-version}
Authorization: Bearer {access-token}
Content-Type: application/json

请求体示例:

{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "2026-08-01T00:00:00Z",
    "to": "2026-08-29T23:59:59Z"
  },
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": {
        "name": "PreTaxCost",
        "function": "Sum"
      }
    }
  }
}

配置时需要注意:

  • 将 granularity 设为 None,避免返回每日明细。
  • 不设置 grouping,避免按资源、meter 或服务拆分结果。
  • 对 PreTaxCost 使用 Sum,由服务端完成汇总。
  • type 设为 ActualCost 时,查询的是实际成本。部分计费协议还支持 AmortizedCost,可按使用期间摊销预留实例等费用。

返回结果通常包含列定义和一行汇总数据,例如:

{
  "properties": {
    "columns": [
      {
        "name": "PreTaxCost",
        "type": "Number"
      },
      {
        "name": "Currency",
        "type": "String"
      }
    ],
    "rows": [
      [
        500.0,
        "USD"
      ]
    ]
  }
}

代码应根据 columns 确定各字段的位置,不宜一直假设金额位于第 0 列。

C# 示例

下面的代码会提交聚合查询,再按列名读取金额和币种。请将 apiVersion 替换为当前环境支持的 Cost Management Query API 版本。

using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public static async Task<(decimal Amount, string Currency)> GetSubscriptionCostAsync(
    string subscriptionId,
    string accessToken,
    DateTimeOffset from,
    DateTimeOffset to,
    string apiVersion)
{
    string requestUrl =
        $"https://management.azure.com/subscriptions/{subscriptionId}" +
        $"/providers/Microsoft.CostManagement/query?api-version={apiVersion}";

    var requestBody = new
    {
        type = "ActualCost",
        timeframe = "Custom",
        timePeriod = new
        {
            from = from.UtcDateTime.ToString("O"),
            to = to.UtcDateTime.ToString("O")
        },
        dataset = new
        {
            granularity = "None",
            aggregation = new
            {
                totalCost = new
                {
                    name = "PreTaxCost",
                    function = "Sum"
                }
            }
        }
    };

    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", accessToken);

    string json = JsonSerializer.Serialize(requestBody);

    using var response = await client.PostAsync(
        requestUrl,
        new StringContent(json, Encoding.UTF8, "application/json"));

    string responseBody = await response.Content.ReadAsStringAsync();
    response.EnsureSuccessStatusCode();

    using JsonDocument document = JsonDocument.Parse(responseBody);

    JsonElement properties = document.RootElement.GetProperty("properties");
    JsonElement columns = properties.GetProperty("columns");
    JsonElement rows = properties.GetProperty("rows");

    if (rows.GetArrayLength() == 0)
    {
        return (0m, string.Empty);
    }

    string[] columnNames = columns
        .EnumerateArray()
        .Select(column => column.GetProperty("name").GetString() ?? string.Empty)
        .ToArray();

    int costIndex = Array.FindIndex(
        columnNames,
        name => string.Equals(name, "PreTaxCost", StringComparison.OrdinalIgnoreCase));

    int currencyIndex = Array.FindIndex(
        columnNames,
        name => string.Equals(name, "Currency", StringComparison.OrdinalIgnoreCase));

    if (costIndex < 0)
    {
        throw new InvalidOperationException(
            "Cost Management response does not contain PreTaxCost.");
    }

    JsonElement row = rows[0];

    decimal amount = row[costIndex].GetDecimal();
    string currency = currencyIndex >= 0
        ? row[currencyIndex].GetString() ?? string.Empty
        : string.Empty;

    return (amount, currency);
}

调用示例:

var result = await GetSubscriptionCostAsync(
    subscriptionId,
    accessToken,
    new DateTimeOffset(2026, 8, 1, 0, 0, 0, TimeSpan.Zero),
    new DateTimeOffset(2026, 8, 29, 23, 59, 59, TimeSpan.Zero),
    apiVersion);

Console.WriteLine($"{result.Amount:N2} {result.Currency}");

如果需要最终发票金额

Cost Management 返回的是成本数据,不能直接视为信用卡或银行账户的最终扣款金额。如果要查询“这张账单最终需要支付多少”,应使用 Azure Billing 的发票资源。

一张发票可能包含:

  • 多个 subscriptions;
  • 税费;
  • 抵扣额或 Azure credits;
  • Marketplace 费用;
  • 调整项;
  • 上期余额;
  • 不同 invoice section 下的费用。

因此,发票总额通常不能直接作为某个 subscription 的总额。如果一张发票覆盖多个订阅,就需要根据成本明细或 invoice section 进行归集。只有计费结构明确为一张发票对应一个订阅时,才能直接将发票金额用于该订阅。

权限与数据时效

调用 API 前还要确认以下事项:

  • 调用方必须拥有读取 subscription 成本数据的权限。仅有资源读取权限,不一定能查看成本。
  • 企业协议、Microsoft Customer Agreement、合作伙伴订阅等计费模式可能采用不同的权限模型。
  • 当月成本通常存在入账延迟,并非严格的实时数据。
  • PreTaxCost 一般不含税费,而最终发票金额可能包含税费和调整项。
  • 免费额度、抵扣、预留实例、Savings Plan、退款和 Marketplace 项目,都可能导致成本汇总与实际付款额不同。
  • 应使用 API 返回的 Currency,不要根据 RateCard 请求中的币种自行推断。
  • 不应继续以早期的 Microsoft.Commerce/UsageAggregates 和 RateCard 预览接口作为新实现的基础。

如果只是展示“该订阅本月累计成本”,使用 Cost Management Query API 执行无粒度、无分组的聚合查询即可。如果需要财务意义上的最终应付金额,则应通过 Billing Invoices API 获取已结算发票。

备注:内容仅供参考。