XML policy fragments + Azure AD app registration for metering across every API. Enforce entitlements and capture usage without modifying your API backends.
Aforo integrates with Azure API Management via native policy fragments — reusable XML blocks that attach to the inbound and outbound policy pipeline of every API in your APIM instance. Azure AD provides service-to-service authentication. Named Values store secrets securely in the APIM key vault.
azure-apim-policy-architecture.flowLIVE
CLIENT
API Request
HTTPS to APIM
Consumer calls APIM gateway URL. TLS terminated at Azure Front Door or APIM directly.
Any tier. Policy fragments, include-fragment and send-one-way-request are available in all of them, Consumption included. Consumption limits a policy document to 16 KiB; the shipped metering fragment is about 14 KB.
REQUIRED
Azure AD tenant with admin access
You need Application Administrator or Global Administrator to create the app registration for service-to-service auth.
REQUIRED
Azure CLI authenticated
az login with a principal that has Contributor on the APIM resource and Key Vault Officer on the key vault.
REQUIRED
Aforo API Key
sk_live_* key from Workspace Admin → API Keys. Will be stored in Azure Key Vault and referenced via Named Values.
OPTIONAL
Azure Key Vault (recommended)
Store the Aforo API key as a Key Vault secret and reference it via an APIM Named Value. Secrets Manager pattern — rotate without redeployment.
Azure APIM Policy Fragments are reusable XML snippets that can be included in any API's policy definition. Create two fragments: one for the inbound entitlement check and one for the outbound metering event. Once created, include them in your API policies with a single line.
The ready-made policy fragments and ARM templates live in the public Aforo SDK repo under aforo-gateway-plugins/azure-apim/ (with azure-arm-templates/ for the Key Vault wiring). Clone or Download ZIP from GitHub, then deploy the fragments below with the Azure CLI:
terminal
git clone https://github.com/aforoai/SDKs.git
cd SDKs/aforo-gateway-plugins/azure-apim
Create the Inbound Fragment (Entitlement)
aforo-entitlement-fragment.xml
<!--
Aforo Inbound Policy Fragment
Purpose: Check tenant entitlement + margin status before forwarding to backend.
Place in: API inbound policy via <include-fragment fragment-id="aforo-entitlement" />
-->
<fragment>
<set-variable name="aforo-tenant-id" value="@(context.Request.Headers.GetValueOrDefault("X-Tenant-Id", ""))" />
<!-- Skip check if no tenant ID (public endpoints) -->
<choose>
<when condition="@(string.IsNullOrEmpty((string)context.Variables["aforo-tenant-id"]))">
<set-variable name="aforo-allowed" value="true" />
</when>
<otherwise>
<!-- Call the Aforo usage-ingestor quota-check endpoint (from the aforo-preflight-url Named Value) -->
<send-request mode="new" response-variable-name="aforo-entitlement-response" timeout="5" ignore-error="true">
<set-url>@(context.Variables.GetValueOrDefault<string>("aforo-preflight-url"))</set-url>
<set-method>GET</set-method>
<set-header name="X-API-Key" exists-action="override">
<value>@((string)context.Variables["aforo-api-key"])</value>
</set-header>
<set-header name="X-Product-Id" exists-action="override">
<value>@(context.Api.Id)</value>
</set-header>
</send-request>
<!-- Parse entitlement response -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<IResponse>("aforo-entitlement-response")?.StatusCode == 200)">
<set-variable name="aforo-allowed" value="@{
var body = ((IResponse)context.Variables["aforo-entitlement-response"]).Body.As<JObject>();
return body["allowed"]?.Value<bool>() ?? true;
}" />
<set-header name="X-Aforo-Remaining" exists-action="override">
<value>@{
var body = ((IResponse)context.Variables["aforo-entitlement-response"]).Body.As<JObject>();
return body["remaining_quota"]?.ToString() ?? "-1";
}</value>
</set-header>
</when>
<!-- On error: fail-open (allow traffic, don't block on Aforo outage) -->
<otherwise>
<set-variable name="aforo-allowed" value="true" />
</otherwise>
</choose>
<!-- Margin Guard L3: Block if allowed=false -->
<choose>
<when condition="@(!(bool)context.Variables["aforo-allowed"])">
<return-response>
<set-status code="429" reason="Margin Limit Exceeded" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{
"error": "margin_limit_exceeded",
"message": "API usage has been suspended. Please contact support.",
"docs": "https://docs.aforo.ai/docs/margin-guards"
}</set-body>
</return-response>
</when>
</choose>
</otherwise>
</choose>
</fragment>
PRO TIP
The ignore-error="true" attribute on send-request is the fail-open mechanism. If Aforo's edge endpoint is unreachable (5xx, timeout), the request proceeds. Combined with the fallback aforo-allowed: true variable, your API never goes dark because of a Aforo outage.
Deploy the Fragment via Azure CLI
The Azure CLI has no command for policy fragments, so call the management API with az rest. Send the XML with format: rawxml: policy expressions contain raw quotes and <, which the default xml format rejects. A fragment holds policy statements only — no <inbound> or <outbound> element, no <base />, and no other fragment inside it.
terminal
# Set your variables
RESOURCE_GROUP="your-rg"
APIM_NAME="your-apim-instance"
SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
APIM="https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_NAME"
# put_fragment <fragment-id> <xml-file> (needs jq)
put_fragment() {
jq -n --rawfile v "$2" '{properties: {format: "rawxml", value: $v}}' > fragment-body.json
az rest --method put \
--url "$APIM/policyFragments/$1?api-version=2022-08-01" \
--body @fragment-body.json
}
# Create the inbound entitlement fragment
put_fragment aforo-entitlement aforo-entitlement-fragment.xml
# Verify the fragment was created
az rest --method get \
--url "$APIM/policyFragments/aforo-entitlement?api-version=2022-08-01" \
--query "{name: name, state: properties.provisioningState}"
Use the shipped fragment as-is: create a policy fragment named aforo-metering whose content is aforo-gateway-plugins/azure-apim/outbound-policy.xml from the <fragment> line on (the comment block above that line is documentation and is not uploaded). It runs in the outbound section, which APIM executes before the response reaches the client, and sends the event with send-one-way-request — fire-and-forget, so the client never waits on Aforo. The trade-off: there is no retry. If the ingest call fails, that request's usage is not recorded.
The fragment reads five Named Values. APIM refuses to save a policy that references a Named Value that doesn't exist, so create all five even if you don't meter MCP traffic:
Named Value
Value
aforo-endpoint
The batch ingest URL: https://api.aforo.ai/v1/ingest/batch
aforo-api-key
Your sk_live_* key — make it a secret, ideally a Key Vault reference
aforo-tenant-id
Your workspace id, sent as X-Tenant-Id
aforo-mcp-enabled
"true" to meter MCP tools/call requests per tool, otherwise "false"
aforo-mcp-product-id
The Aforo MCP Server product id, or any placeholder when MCP is off
Each response produces one event. This excerpt is the standard (non-MCP) branch of the shipped file, trimmed for reading — deploy the full file, which also carries the idempotency key, the outcome classification, the default exclusions (401, 403, 429 and health paths produce no event), trace headers and the MCP branch:
Create the Named Values first, then upload the fragment with the put_fragment helper from the previous section:
terminal
# Everything from the <fragment> line on
sed -n '/^<fragment>/,$p' outbound-policy.xml > aforo-metering.xml
put_fragment aforo-metering aforo-metering.xml
outbound-policy.xml (excerpt)
<send-one-way-request mode="new">
<set-url>{{aforo-endpoint}}</set-url>
<set-method>POST</set-method>
<set-header name="X-API-Key" exists-action="override">
<value>{{aforo-api-key}}</value>
</set-header>
<set-header name="X-Tenant-Id" exists-action="override">
<value>{{aforo-tenant-id}}</value>
</set-header>
<set-body>@{
var eventObj = new JObject(
// The APIM subscription id is the billed customer
new JProperty("customerId", context.Subscription != null ? context.Subscription.Id : "unknown"),
new JProperty("metricName", context.Request.Method + " " + context.Request.Url.Path),
new JProperty("quantity", 1),
// context.RequestId, computed once per request, so a resend is deduplicated
new JProperty("idempotencyKey", context.Variables.GetValueOrDefault("aforo-idempotency-key-base", "")),
new JProperty("occurredAt", DateTime.UtcNow.ToString("o")),
new JProperty("statusCode", context.Response.StatusCode)
/* ...endpointPath, httpMethod, responseTimeMs, trace, metadata... */
);
// Set from the response status; omitted when it can't be determined
var outcome = (string)context.Variables.GetValueOrDefault("aforo-execution-status", "");
if (!String.IsNullOrEmpty(outcome)) { eventObj.Add("executionStatus", outcome); }
return new JObject(new JProperty("events", new JArray(eventObj))).ToString();
}</set-body>
</send-one-way-request>
A request that carries a valid W3C traceparent (or an x-trace-id) header is sent as productType: "AGENTIC_API" with its trace id. With aforo-mcp-enabled set to true, a JSON-RPC tools/call POST is sent instead as an MCP_SERVER event with metric mcp_server.tool_invocations and the tool name.
Include Fragments in Your API Policy
Once fragments are created, adding Aforo to any API is a two-line edit to its policy:
api-policy.xml
<policies>
<inbound>
<base />
<!-- Load the Aforo API key from Named Value (backed by Key Vault) -->
<set-variable name="aforo-api-key" value="{{aforo-api-key}}" />
<!-- Aforo entitlement check + Margin Guard -->
<include-fragment fragment-id="aforo-entitlement" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Aforo async usage metering -->
<include-fragment fragment-id="aforo-metering" />
</outbound>
<on-error>
<base />
</on-error>
</policies>
INFO
Apply this policy at the Product level in APIM (not per-API) to cover all APIs subscribed to that product in a single policy assignment. If you have multiple APIM Products, apply the fragments to each one. The Named Value {{aforo-api-key}} resolves at runtime from your Key Vault reference — the secret is never in the policy XML itself.
Read this if any rate plan behind this gateway uses Outcome-Based pricing — it bills each call at a weight for its executionStatus. Every other pricing model ignores the field. The plugin sets it from the upstream response status:
Upstream status
executionStatus
2xx, 3xx
SUCCESS
408, 504
TIMEOUT
499 (client closed the request, where the gateway reports it)
CANCELLED
400, 422
VALIDATION_FAILED
401, 403, 429
BLOCKED
Every other 4xx and 5xx (404 included)
ERROR
Missing, 0, 1xx
Not sent — the call bills at full weight
WARNING
A status with no weight on the rate plan bills at full weight (1.0). The rate plan wizard starts BLOCKED and CANCELLED at 0, but plans saved before those two statuses were added to the wizard (September 30, 2026) have no weight for them, so the gateway's BLOCKED and CANCELLED calls on those plans bill in full until you give them one — open the plan in the wizard and enter a weight (the fields show blank), or set it through the rate-plan API (outcomeWeights). See Outcome-Based pricing.
Override the status for specific codes
When the default table is wrong for your API — say a 404 is a normal answer, not a failure — map exact codes with the aforo-status-outcomes context variable, set before the fragment is included. Format: comma-separated code=STATUS pairs. It is a context variable, not a Named Value — point it at one (value="{{your-named-value}}") if you want to manage it outside the policy:
Keys are exact HTTP codes from 200 to 599. Values must be one of the 11 statuses: SUCCESS, PARTIAL, TIMEOUT, ERROR, VALIDATION_FAILED, FAILED, FAILURE, CANCELLED, PENDING, BLOCKED, HITL_REQUIRED. Invalid entries are ignored without a warning. If a code is listed twice, the last valid entry wins. An override can't give a status to a missing, 0 or 1xx code.
Requests that aren't metered
By default, responses with status 401, 403 or 429 and requests to /health, /ready or /metrics produce no event at all — they cost nothing whatever the rate plan says. All five gateway plugins share this default. A path is skipped when it equals an entry or starts with it, so /health also covers /health/live.
Setting
Default
To meter everything
Context variable aforo-exclude-status-codes
401,403,429
The word none
Context variable aforo-exclude-paths
/health,/ready,/metrics
The word none
Both are context variables, like aforo-status-outcomes: set them before the fragment is included, or leave them out to get the defaults. A path entry can be written with or without the API URL suffix (/health or /orders-api/health). Entries that aren't whole numbers from 100 to 599 are ignored. A list you set replaces the default rather than adding to it: a status list of 404,500 skips only those two and starts metering 401, 403 and 429. A status you choose to meter bills as BLOCKED unless you override it above.
api-policy.xml
<outbound>
<base />
<!-- also skip 404s and everything under /internal -->
<set-variable name="aforo-exclude-status-codes" value="401,403,429,404" />
<set-variable name="aforo-exclude-paths" value="/health,/ready,/metrics,/internal" />
<include-fragment fragment-id="aforo-metering" />
</outbound>
For production deployments, replace the static API key in Named Values with Azure AD service-to-service authentication. APIM obtains a Bearer token from Azure AD using Client Credentials flow and presents it to Aforo's OAuth2-protected endpoints.
Register the APIM Application in Azure AD
terminal
# Create the app registration for APIM → Aforo service identity
APP_ID=$(az ad app create \
--display-name "Aforo APIM Integration" \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)
echo "App (client) ID: $APP_ID"
# Create a client secret (90-day expiry; rotate before expiry)
CLIENT_SECRET=$(az ad app credential reset \
--id "$APP_ID" \
--years 1 \
--display-name "aforo-apim-secret" \
--query password -o tsv)
echo "Client secret created. Store this in Key Vault immediately."
# Store client ID and secret in Key Vault
az keyvault secret set \
--vault-name "your-keyvault" \
--name "aforo-client-id" \
--value "$APP_ID"
az keyvault secret set \
--vault-name "your-keyvault" \
--name "aforo-client-secret" \
--value "$CLIENT_SECRET"
# Get your Azure AD tenant ID
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "Tenant ID: $TENANT_ID"
Create Named Values in APIM
A policy refers to a Named Value by its display name, which may contain only letters, digits, periods, dashes and underscores — keep it equal to the id. az apim nv create has no Key Vault option, so Key Vault-backed values go through az rest. Give the APIM identity access to the vault first; APIM checks it when the Named Value is created.
terminal
RESOURCE_GROUP="your-rg"
APIM_NAME="your-apim-instance"
VAULT_NAME="your-keyvault"
SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
APIM="https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_NAME"
# 1. Enable the APIM managed identity and let it read the vault
az apim update \
--resource-group "$RESOURCE_GROUP" \
--name "$APIM_NAME" \
--enable-managed-identity true
APIM_IDENTITY=$(az apim show --resource-group "$RESOURCE_GROUP" --name "$APIM_NAME" \
--query identity.principalId -o tsv)
az keyvault set-policy \
--name "$VAULT_NAME" \
--object-id "$APIM_IDENTITY" \
--secret-permissions get list
# 2. Key Vault-backed Named Values: kv_named_value <name> <vault secret name>
kv_named_value() {
az rest --method put \
--url "$APIM/namedValues/$1?api-version=2022-08-01" \
--body "{\"properties\": {\"displayName\": \"$1\", \"secret\": true, \"keyVault\": {\"secretIdentifier\": \"https://$VAULT_NAME.vault.azure.net/secrets/$2\"}}}"
}
kv_named_value aforo-api-key aforo-api-key
kv_named_value aforo-client-secret aforo-client-secret
# 3. Plain Named Values
az apim nv create \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--named-value-id "aforo-client-id" \
--display-name "aforo-client-id" \
--secret false \
--value "$APP_ID"
WARNING
The APIM managed identity must have Key Vault Secrets User role (or the legacy get + list access policy) on the Key Vault. Without this, APIM cannot resolve Named Values backed by Key Vault references and your APIs will return 500 on policy evaluation.
Apply the policy to your APIM API and send a test request to verify end-to-end metering:
Apply the Policy via Azure CLI
terminal
RESOURCE_GROUP="your-rg"
APIM_NAME="your-apim-instance"
API_ID="your-api-id"
SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
APIM="https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_NAME"
# Apply the combined inbound+outbound policy to your API
jq -n --rawfile v api-policy.xml '{properties: {format: "rawxml", value: $v}}' > api-policy-body.json
az rest --method put \
--url "$APIM/apis/$API_ID/policies/policy?api-version=2022-08-01" \
--body @api-policy-body.json
# Verify the policy was applied
az rest --method get \
--url "$APIM/apis/$API_ID/policies/policy?api-version=2022-08-01&format=rawxml" \
--query properties.value -o tsv | grep -o "include-fragment[^/]*"
# Expected output:
# include-fragment fragment-id="aforo-entitlement"
# include-fragment fragment-id="aforo-metering"
Send a Test Request
terminal
# Get your APIM gateway URL
GATEWAY_URL=$(az apim show \
--resource-group "$RESOURCE_GROUP" \
--name "$APIM_NAME" \
--query gatewayUrl -o tsv)
# Make a test request (replace with a real subscription key)
curl -v "$GATEWAY_URL/your-api/endpoint" \
-H "Ocp-Apim-Subscription-Key: your_subscription_key" \
-H "X-Tenant-Id: test_tenant_123"
# Check for Aforo response headers:
# X-Aforo-Remaining: 8420 ← quota remaining for this tenant
# X-Aforo-Plan: enterprise ← plan tier
# Then confirm it arrived: in Aforo, open Intelligence → Usage Ingestion.
# The event shows up once the plugin has sent it (batching plugins flush every few seconds).
AZURE APIM TRACE — READING THE POLICY EXECUTION LOG
Enable APIM tracing with Ocp-Apim-Trace: true and Ocp-Apim-Subscription-Key on your request. The trace response includes the policy execution timeline — look for the aforo-entitlement fragment in the inbound section and the aforo-metering fragment in the outbound section. Each step shows duration, variables set, and any errors. Available in the Azure Portal under APIM → APIs → Test → Trace.
Common Issues
NamedValue resolve failedAPIM managed identity missing Key Vault read permission. Run the az keyvault set-policy command above.
include-fragment not foundThe fragment-id in the policy must exactly match the fragment you created. List them with az rest --method get --url "$APIM/policyFragments?api-version=2022-08-01". A fragment write is asynchronous: wait until it reads back before saving a policy that includes it.
APIM rejects the fragmentA fragment can hold policy statements only. Remove any <inbound> / <outbound> wrapper or <base />, do not include another fragment from it, and upload with format rawxml. Copies of outbound-policy.xml from before 2026-10-01 had an <outbound> wrapper; use the current file.
Entitlement check returns 401The Named Value aforo-api-key is not resolving correctly. Test with az apim nv show to verify the Key Vault reference is healthy.
401 / 403 / 429 or health checks missing from usageExpected: the fragment skips those by default. Set the context variable aforo-exclude-status-codes (or aforo-exclude-paths) before the include-fragment — a list replaces the default, and the word none meters everything.
No events in Aforo after requestCheck the outbound fragment is in the correct policy section. Use APIM Trace to confirm the send-one-way-request in the outbound section ran. It does not wait for a response, so the trace shows the send, not the result.
PRO TIP
The fragments use policies that are available in every APIM tier on the Azure-managed gateway. On a self-hosted gateway, Microsoft documents that more than one send-one-way-request in the outbound section is not supported, and the metering fragment has two (one for MCP calls, one for everything else), so test it on your gateway before relying on it. Also ensure the gateway pod has outbound HTTPS access to the hosts your Named Values point at — the ingest host in {{aforo-endpoint}} and the quota-check host in aforo-preflight-url (both on *.aforo.ai).