Sign in →
1 min read

Azure APIM Integration

XML policy fragments + Azure AD app registration for metering across every API. Enforce entitlements and capture usage without modifying your API backends.

Updated 2026-11-11Suggest edits

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.
0ms
INBOUND
Entitlement
inbound policy
send-request to Aforo edge. Workspace quota + margin check. set-variable stores decision.
<5ms
BACKEND
API Backend
your service
Request forwarded to backend. HTTP Integration or Service URL target.
passthrough
OUTBOUND
Metering
outbound policy
send-one-way-request to Aforo ingest — fire-and-forget, the client never waits on it.
0ms added
NAMED VALUES
Secrets
key vault ref
APIM Named Values resolve aforo-api-key from Azure Key Vault reference.
cached
INGEST
Aforo
api.aforo.ai
HTTPS POST. API key in the X-API-Key header. Idempotent retry.
<60ms
azure apim inbound/outbound policy · azure ad service principal auth · named values for secrets// p95 < 5ms

Prerequisites#

REQUIRED
Azure API Management instance
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.

Policy Fragment Setup#

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}"

Outbound Policy#

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 ValueValue
aforo-endpointThe batch ingest URL: https://api.aforo.ai/v1/ingest/batch
aforo-api-keyYour sk_live_* key — make it a secret, ideally a Key Vault reference
aforo-tenant-idYour workspace id, sent as X-Tenant-Id
aforo-mcp-enabled"true" to meter MCP tools/call requests per tool, otherwise "false"
aforo-mcp-product-idThe 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.

How Calls Get an Outcome#

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 statusexecutionStatus
2xx, 3xxSUCCESS
408, 504TIMEOUT
499 (client closed the request, where the gateway reports it)CANCELLED
400, 422VALIDATION_FAILED
401, 403, 429BLOCKED
Every other 4xx and 5xx (404 included)ERROR
Missing, 0, 1xxNot 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:

api-policy.xml
<outbound>
    <base />
    <set-variable name="aforo-status-outcomes" value="404=SUCCESS,429=ERROR" />
    <include-fragment fragment-id="aforo-metering" />
</outbound>

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.

SettingDefaultTo meter everything
Context variable aforo-exclude-status-codes401,403,429The word none
Context variable aforo-exclude-paths/health,/ready,/metricsThe 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>

App Registration#

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.

Deploy & Test#

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).
Aforo Documentation — Aforo Docs