XML policy fragment + Azure AD app registration for enterprise-grade metering. 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.
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.
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 Aforo edge entitlement endpoint -->
<send-request mode="new" response-variable-name="aforo-entitlement-response" timeout="5" ignore-error="true">
<set-url>@("https://edge.aforo.ai/v1/check/" + context.Variables["aforo-tenant-id"])</set-url>
<set-method>GET</set-method>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + 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/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
terminal
# Set your variables
RESOURCE_GROUP="your-rg"
APIM_NAME="your-apim-instance"
SUBSCRIPTION_ID="your-subscription-id"
# Create the inbound entitlement fragment
az apim policy fragment create \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--name "aforo-entitlement" \
--description "Aforo tenant entitlement check + Margin Guard L1-L3 enforcement" \
--value "$(cat aforo-entitlement-fragment.xml)"
# Verify the fragment was created
az apim policy fragment show \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--name "aforo-entitlement" \
--query "{name: name, description: description}"
The outbound policy fragment fires after the backend response is received and after the response is returned to the client. Azure APIM's send-request in the outbound section runs asynchronously — the consumer's connection is not held open while the metering event is dispatched.
aforo-metering-fragment.xml
<!--
Aforo Outbound Policy Fragment
Purpose: Dispatch async usage event to Aforo ingest after response is returned.
Place in: API outbound policy via <include-fragment fragment-id="aforo-metering" />
-->
<fragment>
<!-- Only meter if tenant ID was present on the request -->
<choose>
<when condition="@(!string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("aforo-tenant-id", "")))">
<send-request mode="new" response-variable-name="aforo-ingest-response" timeout="10" ignore-error="true">
<set-url>https://ingest.aforo.ai/v1/ingest</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + context.Variables["aforo-api-key"])</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Idempotency-Key" exists-action="override">
<value>@(context.RequestId)</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("tenantId", context.Variables["aforo-tenant-id"]),
new JProperty("productId", context.Api.Id),
new JProperty("metricId", "api-calls"),
new JProperty("quantity", 1),
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("metadata", new JObject(
new JProperty("method", context.Request.Method),
new JProperty("path", context.Request.Url.Path),
new JProperty("statusCode", context.Response.StatusCode),
new JProperty("durationMs", context.Elapsed.TotalMilliseconds),
new JProperty("requestId", context.RequestId),
new JProperty("apiVersion", context.Api.Version),
new JProperty("operationId", context.Operation.Id)
))
).ToString();
}</set-body>
</send-request>
</when>
</choose>
</fragment>
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.
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
terminal
RESOURCE_GROUP="your-rg"
APIM_NAME="your-apim-instance"
KEYVAULT_ID="/subscriptions/SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/your-keyvault"
# Named Value: Aforo API key (Key Vault reference)
az apim nv create \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--named-value-id "aforo-api-key" \
--display-name "Aforo API Key" \
--secret true \
--key-vault-secret-identifier "$KEYVAULT_ID/secrets/aforo-api-key"
# Named Value: Azure AD client ID (plain text — not secret)
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"
# Named Value: Azure AD client secret (Key Vault reference)
az apim nv create \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--named-value-id "aforo-client-secret" \
--display-name "Aforo Client Secret" \
--secret true \
--key-vault-secret-identifier "$KEYVAULT_ID/secrets/aforo-client-secret"
# Enable the APIM managed identity (required for Key Vault access)
az apim update \
--resource-group "$RESOURCE_GROUP" \
--name "$APIM_NAME" \
--enable-managed-identity true
# Grant APIM managed identity access to Key Vault
APIM_IDENTITY=$(az apim show --resource-group "$RESOURCE_GROUP" --name "$APIM_NAME" \
--query identity.principalId -o tsv)
az keyvault set-policy \
--name "your-keyvault" \
--object-id "$APIM_IDENTITY" \
--secret-permissions get list
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"
# Apply the combined inbound+outbound policy to your API
az apim api policy create \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--api-id "$API_ID" \
--policy-content "$(cat api-policy.xml)"
# Verify the policy was applied
az apim api policy show \
--resource-group "$RESOURCE_GROUP" \
--service-name "$APIM_NAME" \
--api-id "$API_ID" | 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
# Verify event arrived in Aforo (~5 seconds after request)
curl -s "https://api.aforo.ai/v1/events?tenant_id=test_tenant_123&limit=1" \
-H "Authorization: Bearer sk_live_your_admin_key" | jq '{
tenantId: .data[0].tenantId,
productId: .data[0].productId,
quantity: .data[0].quantity,
timestamp: .data[0].timestamp
}'
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 foundFragment name in policy XML must exactly match the fragment-id used in az apim policy fragment create. Check with az apim policy fragment list.
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.
No events in Aforo after requestCheck the outbound fragment is in the correct policy section. Use APIM Trace to confirm the send-request in the outbound section executed and returned 200.
PRO TIP
The Azure APIM integration supports all APIM tiers (Developer, Standard, Premium) and works with both self-hosted gateways and the Azure-managed gateway. For self-hosted gateways, ensure the gateway pod has outbound HTTPS access to ingest.aforo.ai and edge.aforo.ai.