← All posts
AzureAPI ManagementAI GatewayMicrosoft EntraMicrosoft FoundryClaude CodeCodexEnterprise AI

Using Azure API Management as an Enterprise AI Gateway for Claude Code and Codex

A technical walkthrough for using Azure API Management, Microsoft Entra authentication, managed identity, and Log Analytics to govern Claude Code and Codex traffic in the enterprise.

AI coding assistants are easy to pilot and harder to govern.

An individual developer can usually get a tool working with a local API key, a base URL, and a few environment variables. That is fine for experimentation. It is not enough for enterprise deployment.

Once tools like Claude Code and Codex become part of the standard developer workflow, the questions change:

How do we authenticate users?
How do we avoid distributing static model API keys?
How do we route traffic through approved infrastructure?
How do we apply per-user or per-team controls?
How do we report usage by user, department, model, and business unit?
How do we preserve the developer experience while adding governance?

One pattern that works well is to use Azure API Management (APIM) as an enterprise AI Gateway in front of Microsoft Foundry.

In this model, Claude Code and Codex still run locally for developers, but their traffic flows through APIM. Users authenticate with Microsoft Entra. APIM validates the user, applies policy, strips unsafe headers, routes to the correct Foundry backend, calls Foundry with managed identity, and writes telemetry to Log Analytics.

The key principle is:

User identity authenticates to APIM.
APIM identity authenticates to Foundry.

That separation is what makes the pattern useful. Developers do not need backend API keys on their machines. Foundry does not need to trust every endpoint. The enterprise gets a central control plane for authentication, routing, governance, quotas, and observability.

Reference architecture

At a high level, the flow looks like this:

Developer machine
  Claude Code / Codex
    -> Azure CLI token for https://ai.azure.com
    -> Authorization: Bearer <Entra token>

Azure API Management
  -> validate Microsoft Entra JWT
  -> extract user identity claims
  -> set reporting headers
  -> remove client API-key headers
  -> apply rate/token policy
  -> route to Foundry API/backend
  -> authenticate to Foundry with APIM managed identity

Microsoft Foundry
  -> receives request from APIM
  -> returns model response

Azure Monitor / Log Analytics
  -> ApiManagementGatewayLogs
  -> ApiManagementGatewayLlmLog
  -> join by CorrelationId

Here is the same thing as a Mermaid diagram:

flowchart LR
    Dev[Developer Machine] --> CC[Claude Code / Codex]
    CC -->|az account get-access-token<br/>resource: https://ai.azure.com| Token[Microsoft Entra Token]
    CC -->|Authorization: Bearer token| APIM[Azure API Management]

    APIM --> JWT[validate-jwt]
    JWT --> Claims[Extract oid / upn / aud]
    Claims --> Headers[Set reporting headers]
    Headers --> Cleanup[Strip client API-key headers]
    Cleanup --> Policy[Apply quota / routing policy]
    Policy -->|Managed identity| Foundry[Microsoft Foundry]

    APIM --> Logs[Log Analytics]
    Logs --> Gateway[ApiManagementGatewayLogs]
    Logs --> LLM[ApiManagementGatewayLlmLog]
    Gateway --> Join[Join by CorrelationId]
    LLM --> Join

Why APIM is the right control point

Direct-to-model access works, but it creates governance problems:

  • Static API keys are hard to rotate, audit, and attribute.
  • Local configuration can drift across machines.
  • Direct model traffic bypasses centralized controls.
  • Usage reporting by user, department, model, or business unit is difficult.
  • Quotas and disablement need a consistent enforcement point.
  • Different groups may need separate Foundry resources or routing paths.

APIM gives you a central policy layer where you can:

  • Validate Microsoft Entra JWTs.
  • Extract stable identity claims.
  • Route to different Foundry APIs and backends.
  • Strip stale or unsafe client credential headers.
  • Apply rate limits and token quotas.
  • Use managed identity for backend access.
  • Log gateway and LLM token telemetry to Log Analytics.
  • Enrich logs with user, client, department, manager, or API-route metadata.

In other words:

APIM is not just a proxy.
APIM is the enterprise control plane for AI traffic.

The APIM request flow

Every request should move through the gateway in a predictable order:

1. Client sends request to APIM
   Authorization: Bearer <Entra token>

2. APIM validates the JWT
   - signature
   - issuer
   - audience
   - tenant

3. APIM extracts identity claims
   - oid
   - upn / preferred_username
   - aud

4. APIM creates reporting variables and headers
   - x-entra-oid
   - x-entra-upn
   - x-client-type

5. APIM removes client credential headers
   - x-api-key
   - api-key
   - anthropic-api-key
   - Ocp-Apim-Subscription-Key

6. APIM applies governance policy
   - token quotas
   - rate limits
   - optional user disablement
   - route-specific controls

7. APIM forwards to Foundry using managed identity

8. APIM emits gateway and LLM token logs

9. Log Analytics joins request identity with token usage by CorrelationId

That flow is the heart of the architecture.

Client authentication with Azure CLI

The developer signs in locally with Azure CLI:

az login --tenant <tenant-id>

If the user does not have Azure subscriptions in that tenant, use:

az login --tenant <tenant-id> --allow-no-subscriptions

The local helper then requests a token for Azure AI:

az account get-access-token \
  --tenant <tenant-id> \
  --resource https://ai.azure.com \
  --query accessToken \
  -o tsv

The client sends that token to APIM:

Authorization: Bearer <access-token>

This token is for APIM validation. It proves who the user is. It is not the credential APIM uses to call Foundry.

Claude Code managed settings

Claude Code supports managed settings, which makes it suitable for enterprise deployment through tools like Jamf, Intune, Group Policy, or other device management systems.

The managed configuration should:

  • Point Claude Code at the APIM Foundry gateway URL.
  • Configure an apiKeyHelper.
  • Set model defaults.
  • Avoid static API keys.

Example managed settings:

{
  "apiKeyHelper": "/usr/local/bin/claude-azure-ai-token",
  "env": {
    "CLAUDE_CODE_USE_FOUNDRY": "1",
    "ANTHROPIC_FOUNDRY_BASE_URL": "https://<apim-name>.azure-api.net/<foundry-api-path>/anthropic",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
    "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "300000"
  },
  "companyAnnouncements": [
    "Claude Code is configured to use Microsoft Foundry through the enterprise APIM gateway. Run az login before starting Claude Code."
  ]
}

The helper script only needs to output the access token:

#!/bin/sh
set -eu

az account get-access-token \
  --tenant <tenant-id> \
  --resource https://ai.azure.com \
  --query accessToken \
  -o tsv

Common managed settings locations:

macOS:
/Library/Application Support/ClaudeCode/managed-settings.d/10-foundry-apim.json

Windows:
C:\Program Files\ClaudeCode\managed-settings.json

On Windows, keep the helper itself under a path without spaces:

C:\ProgramData\ClaudeCode\claude-azure-ai-token.cmd

Avoid pointing apiKeyHelper at a path under:

C:\Program Files\...

Some clients invoke helpers as command strings. A space in the path can prevent execution, causing APIM to receive a request with no bearer token.

In APIM logs, that usually looks like:

401 TokenNotPresent
JWT not present

Codex provider configuration

Codex can use the same Entra-token pattern through its provider auth command.

Example configuration:

model = "gpt-5.5"
model_provider = "enterprise-codex"
model_reasoning_effort = "medium"

[model_providers.enterprise-codex]
name = "Enterprise Codex via APIM"
base_url = "https://<apim-name>.azure-api.net/<openai-api-path>/openai/v1"
wire_api = "responses"

[model_providers.enterprise-codex.auth]
command = "/usr/local/bin/codex-azure-openai-token"
timeout_ms = 5000
refresh_interval_ms = 300000

The auth command returns the Azure AI token:

az account get-access-token \
  --tenant <tenant-id> \
  --resource https://ai.azure.com \
  --query accessToken \
  -o tsv

This gives Claude Code and Codex the same enterprise authentication model:

Local Entra sign-in.
Bearer token to APIM.
APIM managed identity to Foundry.

APIM policy: validate the JWT

At the gateway, validate the incoming bearer token before forwarding anything to Foundry.

<validate-jwt header-name="Authorization"
              require-scheme="Bearer"
              failed-validation-httpcode="401"
              failed-validation-error-message="Unauthorized. Run az login and try again."
              output-token-variable-name="user-jwt">
  <openid-config url="https://login.microsoftonline.com/<tenant-id>/.well-known/openid-configuration" />
  <audiences>
    <audience>https://ai.azure.com</audience>
  </audiences>
  <issuers>
    <issuer>https://sts.windows.net/<tenant-id>/</issuer>
    <issuer>https://login.microsoftonline.com/<tenant-id>/v2.0</issuer>
  </issuers>
</validate-jwt>

For Claude Code and Codex using Azure CLI tokens, the important audience is:

https://ai.azure.com

If you also route an SSO-based desktop client through the same gateway, you can add that Entra application client ID as another accepted audience and map it to a different client type.

APIM policy: extract identity claims

After validate-jwt succeeds, APIM can read claims from the token.

<set-variable name="userOid"
              value="@(((Jwt)context.Variables[&quot;user-jwt&quot;]).Claims.GetValueOrDefault(&quot;oid&quot;, &quot;unknown&quot;))" />

<set-variable name="userUpn"
              value="@(((Jwt)context.Variables[&quot;user-jwt&quot;]).Claims.GetValueOrDefault(&quot;upn&quot;, ((Jwt)context.Variables[&quot;user-jwt&quot;]).Claims.GetValueOrDefault(&quot;preferred_username&quot;, &quot;unknown&quot;)))" />

<set-variable name="tokenAudience"
              value="@(((Jwt)context.Variables[&quot;user-jwt&quot;]).Claims.GetValueOrDefault(&quot;aud&quot;, &quot;unknown&quot;))" />

<set-variable name="clientType"
              value="@(((string)context.Variables[&quot;tokenAudience&quot;]) == &quot;https://ai.azure.com&quot; ? &quot;Claude Code / Codex&quot; : &quot;Other AI Client&quot;)" />

The most important claim is:

oid

That is the Microsoft Entra object ID. It is stable and works well for quota keys, joins, and reporting. UPNs and email addresses are useful for human-readable reports, but they can change.

APIM policy: add loggable identity headers

APIM diagnostics can capture selected request and response headers. A useful pattern is to expose identity and client metadata as response headers, then configure diagnostics to log those headers.

<set-header name="x-entra-oid" exists-action="override">
  <value>@((string)context.Variables[&quot;userOid&quot;])</value>
</set-header>

<set-header name="x-entra-upn" exists-action="override">
  <value>@((string)context.Variables[&quot;userUpn&quot;])</value>
</set-header>

<set-header name="x-client-type" exists-action="override">
  <value>@((string)context.Variables[&quot;clientType&quot;])</value>
</set-header>

These headers are not for Foundry. They are for observability.

Once logged, they let you answer:

  • Who used the gateway?
  • Which client did they use?
  • Which APIM API path did they hit?
  • How many tokens did they consume?
  • Which departments or business units are driving usage?

APIM policy: capture response usage and cache tokens

ApiManagementGatewayLlmLog records prompt, completion, and total tokens, but the live Anthropic response contains a richer usage object that includes prompt-cache read and write token counts.

Example response usage:

{
  "cache_creation": {
    "ephemeral_1h_input_tokens": 0,
    "ephemeral_5m_input_tokens": 5464
  },
  "cache_creation_input_tokens": 5464,
  "cache_read_input_tokens": 0,
  "input_tokens": 9,
  "output_tokens": 1,
  "service_tier": "standard"
}

That means APIM can capture request-level cache telemetry without logging prompt or completion bodies. The outbound policy can inspect successful responses with preserveContent: true and write the numeric fields into response headers. For Claude Code, support both non-streaming JSON and streaming text/event-stream responses because the usage object can appear inside SSE data: {...} events:

x-llm-input-tokens
x-llm-output-tokens
x-llm-cache-write-tokens
x-llm-cache-read-tokens
x-llm-cache-write-5m-tokens
x-llm-cache-write-1h-tokens
x-llm-service-tier
x-llm-inference-geo

Those headers can then be captured by APIM diagnostics and joined back to identity and token rows with CorrelationId.

APIM policy: strip client credential headers

This is one of the most important parts of the design.

APIM is the authentication boundary. Users should not send static API keys or APIM subscription keys through to Foundry.

Remove API-key style headers before backend forwarding:

<set-header name="x-api-key" exists-action="delete" />
<set-header name="api-key" exists-action="delete" />
<set-header name="anthropic-api-key" exists-action="delete" />
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />

This protects against stale local configuration. A user may have an old direct-Foundry or direct-Anthropic setting on their machine. Without header cleanup, APIM may validate the user's Entra token successfully, but Foundry may still reject the backend call because it received conflicting client auth headers.

The rule should be:

Client credentials stop at APIM.
Backend credentials are APIM managed identity.

Backend authentication with managed identity

Each Foundry resource can be configured as an APIM backend.

Backend ID:
<foundry-resource-name>

Backend URL:
https://<foundry-resource-name>.services.ai.azure.com/

Managed identity resource:
https://ai.azure.com/

The APIM policy selects the backend:

<set-backend-service backend-id="<foundry-backend-id>" />

Grant APIM's managed identity access to the Foundry resource. For example:

Cognitive Services User

This is what lets APIM call Foundry without exposing backend credentials to users or endpoints.

Routing multiple Foundry targets

You can expose separate APIM API paths for different Foundry resources, teams, or business units.

Example Anthropic-compatible routes:

https://<apim-name>.azure-api.net/team-a-foundry/anthropic
https://<apim-name>.azure-api.net/team-b-foundry/anthropic
https://<apim-name>.azure-api.net/team-c-foundry/anthropic

Example OpenAI-compatible routes for Codex:

https://<apim-name>.azure-api.net/team-a-openai/openai/v1
https://<apim-name>.azure-api.net/team-b-openai/openai/v1
https://<apim-name>.azure-api.net/team-c-openai/openai/v1

The route becomes a useful reporting dimension. Even if users never label sessions or use cases manually, APIM can infer the target from ApiId.

Diagnostics and Log Analytics

For this scenario, two APIM log tables matter most:

ApiManagementGatewayLogs
  Request and response metadata, status codes, caller IP, headers, CorrelationId

ApiManagementGatewayLlmLog
  Model/deployment usage, prompt tokens, completion tokens, total tokens, CorrelationId

The bridge between the two is:

CorrelationId

To make identity fields available in ApiManagementGatewayLogs, configure APIM diagnostics to capture the response headers set by policy.

Example diagnostic fragment:

{
  "properties": {
    "frontend": {
      "response": {
        "headers": [
          "x-entra-oid",
          "x-entra-upn",
          "x-client-type",
          "x-llm-input-tokens",
          "x-llm-output-tokens",
          "x-llm-cache-write-tokens",
          "x-llm-cache-read-tokens",
          "x-llm-cache-write-5m-tokens",
          "x-llm-cache-write-1h-tokens",
          "x-llm-service-tier",
          "x-llm-inference-geo"
        ]
      }
    }
  }
}

If you choose to log custom request headers, configure frontend.request.headers instead.

For AI traffic, be careful with body logging. If the goal is token accounting and attribution, you usually do not need to log prompt and completion content. Identity metadata plus token telemetry is often enough for operational reporting.

Query identity and token usage

Once diagnostics are configured, join gateway logs to LLM token logs by CorrelationId.

let gateway =
ApiManagementGatewayLogs
| where TimeGenerated > ago(24h)
| where _ResourceId has "<apim-name>"
| extend
    EntraOID = tostring(ResponseHeaders["x-entra-oid"]),
    EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
    ClientType = tostring(ResponseHeaders["x-client-type"]),
    LlmInputTokens = toint(ResponseHeaders["x-llm-input-tokens"]),
    LlmOutputTokens = toint(ResponseHeaders["x-llm-output-tokens"]),
    LlmCacheWriteTokens = toint(ResponseHeaders["x-llm-cache-write-tokens"]),
    LlmCacheReadTokens = toint(ResponseHeaders["x-llm-cache-read-tokens"])
| project
    CorrelationId,
    ApiId,
    OperationId,
    ResponseCode,
    BackendResponseCode,
    CallerIpAddress,
    EntraOID,
    EntraUPN,
    ClientType,
    LlmInputTokens,
    LlmOutputTokens,
    LlmCacheWriteTokens,
    LlmCacheReadTokens;

ApiManagementGatewayLlmLog
| where TimeGenerated > ago(24h)
| where _ResourceId has "<apim-name>"
| join kind=inner gateway on CorrelationId
| summarize
    PromptTokens = sum(PromptTokens),
    CompletionTokens = sum(CompletionTokens),
    TotalTokens = sum(TotalTokens),
    ResponseInputTokens = sum(LlmInputTokens),
    ResponseOutputTokens = sum(LlmOutputTokens),
    CacheWriteTokens = sum(LlmCacheWriteTokens),
    CacheReadTokens = sum(LlmCacheReadTokens),
    Requests = count()
  by EntraUPN, EntraOID, ClientType, ApiId
| order by TotalTokens desc

That query gives a practical enterprise usage view:

  • user
  • client type
  • APIM API/backend
  • request count
  • prompt tokens
  • completion tokens
  • total tokens
  • cache write tokens
  • cache read tokens

Pricing the usage data

The APIM headers give you request-level usage. Azure Cost Management gives you the billing truth.

That distinction matters. APIM can tell you that Jordan used 10,000 input tokens, 2,000 output tokens, 50,000 cache write tokens, and 200,000 cache read tokens. Cost Management tells you what Azure actually charged for those meter types on that model, deployment, and day.

The pricing model is:

estimated cost =
  input_tokens       / 1,000,000 * input_rate_per_1M
+ output_tokens      / 1,000,000 * output_rate_per_1M
+ cache_write_tokens / 1,000,000 * cache_write_rate_per_1M
+ cache_read_tokens  / 1,000,000 * cache_read_rate_per_1M

The response headers map to Cost Management meters like this:

APIM response header Cost Management meter
x-llm-input-tokens Input tokens
x-llm-output-tokens Output tokens
x-llm-cache-write-tokens Cache write tokens
x-llm-cache-read-tokens Cache hit/read tokens

I would not hardcode these rates in APIM. Pull the effective rates from Cost Management, because Azure can expose different meters for regular context, long context, cache writes, cache hits, and model-specific pricing.

The effective rate calculation is:

effective_rate_per_1M_tokens = PreTaxCost / UsageQuantity * 1,000,000

Then apply those rates to the APIM usage totals.

Example APIM-side daily usage query:

let gateway =
ApiManagementGatewayLogs
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| extend
    EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
    InputTokens = tolong(ResponseHeaders["x-llm-input-tokens"]),
    OutputTokens = tolong(ResponseHeaders["x-llm-output-tokens"]),
    CacheWriteTokens = tolong(ResponseHeaders["x-llm-cache-write-tokens"]),
    CacheReadTokens = tolong(ResponseHeaders["x-llm-cache-read-tokens"]);
ApiManagementGatewayLlmLog
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| join kind=inner gateway on CorrelationId
| summarize
    Requests = count(),
    InputTokens = sum(InputTokens),
    OutputTokens = sum(OutputTokens),
    CacheWriteTokens = sum(CacheWriteTokens),
    CacheReadTokens = sum(CacheReadTokens)
  by bin(TimeGenerated, 1d), ApiId, EntraUPN, ModelName, DeploymentName
| order by TimeGenerated desc, ApiId asc, EntraUPN asc

If you want to model costs directly in Log Analytics, you can load the current rates into a small datatable or external table and calculate estimated cost:

let rates = datatable(ModelName:string, InputPer1M:real, OutputPer1M:real, CacheWritePer1M:real, CacheReadPer1M:real)
[
  "claude-opus-4-8", 0.0, 0.0, 0.0, 0.0,
  "claude-sonnet-4-6", 0.0, 0.0, 0.0, 0.0,
  "claude-haiku-4-5", 0.0, 0.0, 0.0, 0.0
];
let usage =
ApiManagementGatewayLogs
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| extend
    EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
    InputTokens = tolong(ResponseHeaders["x-llm-input-tokens"]),
    OutputTokens = tolong(ResponseHeaders["x-llm-output-tokens"]),
    CacheWriteTokens = tolong(ResponseHeaders["x-llm-cache-write-tokens"]),
    CacheReadTokens = tolong(ResponseHeaders["x-llm-cache-read-tokens"])
| join kind=inner (
    ApiManagementGatewayLlmLog
    | where TimeGenerated > ago(30d)
    | project CorrelationId, ModelName, DeploymentName
  ) on CorrelationId
| summarize
    InputTokens = sum(InputTokens),
    OutputTokens = sum(OutputTokens),
    CacheWriteTokens = sum(CacheWriteTokens),
    CacheReadTokens = sum(CacheReadTokens)
  by EntraUPN, ModelName, DeploymentName;
usage
| join kind=leftouter rates on ModelName
| extend
    EstimatedInputCost = InputTokens / 1000000.0 * InputPer1M,
    EstimatedOutputCost = OutputTokens / 1000000.0 * OutputPer1M,
    EstimatedCacheWriteCost = CacheWriteTokens / 1000000.0 * CacheWritePer1M,
    EstimatedCacheReadCost = CacheReadTokens / 1000000.0 * CacheReadPer1M
| extend EstimatedTotalCost = EstimatedInputCost + EstimatedOutputCost + EstimatedCacheWriteCost + EstimatedCacheReadCost
| order by EstimatedTotalCost desc

The clean enterprise pattern is:

Cost Management = invoice and meter truth
APIM headers    = user, route, request, and cache-token attribution

In practice, I would reconcile them daily:

  1. Export Cost Management by day, resource, product, meter, and cost.
  2. Map hidden Microsoft.SaaS/resources/claude-* rows back to the Foundry account using the Foundry account internal ID embedded in the SaaS resource name.
  3. Calculate effective rates by model and meter.
  4. Apply those rates to APIM token totals by user, API, model, and day.
  5. Treat the current day as partial because Cost Management can lag.

Optional Microsoft Graph enrichment

JWT claims are enough for basic attribution. If you need richer reporting, APIM can call Microsoft Graph using managed identity and enrich the logged headers with directory attributes.

Examples:

x-entra-display-name
x-entra-mail
x-entra-user-principal-name
x-entra-job-title
x-entra-department
x-entra-company-name
x-entra-manager-upn

This enables reporting by department, manager, title, or business unit.

Use this intentionally. These attributes are personally identifiable information and will be written to Log Analytics if captured by diagnostics. Only add fields that are genuinely useful for reporting or governance.

Enterprise deployment pattern

Package the client configuration as managed deployment scripts:

  • macOS shell scripts for Jamf or Intune.
  • Windows PowerShell scripts for Intune, ConfigMgr, GPO, or elevated local install.
  • Separate target variants for different APIM routes.
  • Codex-only scripts for machines that do not need Claude Code.
  • Cleanup logic for stale direct-Foundry settings and API keys.

The deployment scripts should:

  1. Ensure Azure CLI is installed.
  2. Install the token helper.
  3. Write managed Claude Code settings.
  4. Write Codex provider configuration if needed.
  5. Remove stale API-key or direct-Foundry environment variables.
  6. Print validation commands.

The end-user workflow should remain simple:

az login --tenant <tenant-id>
claude

Or:

az login --tenant <tenant-id>
codex

No static model API key is required.

Validation

Validate that Azure CLI can return the expected token:

az account get-access-token \
  --tenant <tenant-id> \
  --resource https://ai.azure.com \
  --query "{expiresOn:expiresOn, tenant:tenant, tokenType:tokenType}" \
  -o table

Validate macOS helpers:

/usr/local/bin/claude-azure-ai-token | cut -c1-30
/usr/local/bin/codex-azure-openai-token | cut -c1-30

Validate Windows helpers:

& "$env:ProgramData\ClaudeCode\claude-azure-ai-token.cmd"
& "$env:ProgramData\ClaudeCode\codex-azure-openai-token.cmd"

Validate the configured Windows helper path:

(Get-Content "$env:ProgramFiles\ClaudeCode\managed-settings.json" | ConvertFrom-Json).apiKeyHelper

Expected:

C:\ProgramData\ClaudeCode\claude-azure-ai-token.cmd

Validate the APIM path directly:

TOKEN="$(az account get-access-token \
  --tenant <tenant-id> \
  --resource https://ai.azure.com \
  --query accessToken \
  -o tsv)"

curl -sS \
  -X POST "https://<apim-name>.azure-api.net/<foundry-api-path>/anthropic/v1/messages" \
  -H "Authorization: Bearer $TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  --data '{
    "model": "claude-haiku-4-5",
    "max_tokens": 16,
    "messages": [
      {
        "role": "user",
        "content": "Reply with exactly: gateway-ok"
      }
    ]
  }'

Troubleshooting

Symptom Likely cause What to check
401 TokenNotPresent / JWT not present Claude Code did not send a bearer token apiKeyHelper path, helper execution, spaces in Windows paths
401 with token present Token validation failed aud, iss, tenant ID, accepted audiences
APIM auth succeeds but backend returns 401 Foundry rejected backend auth APIM managed identity role and stripped client API-key headers
Old Foundry resource is used Stale local environment variables Remove direct Foundry resource/base URL/API-key settings
Direct helper works but Claude fails Claude did not reload settings/env Restart terminal and Claude Code
Identity headers missing in Log Analytics Diagnostics not capturing those headers APIM diagnostic frontend.response.headers
Token logs do not join to gateway logs Missing or mismatched correlation IDs/time range Query both tables over same time window

What this pattern gets you

This architecture gives enterprises a clean foundation for AI coding assistant governance:

No static model API keys on developer machines.
No direct backend access from endpoints.
Centralized authentication and policy.
Managed identity from APIM to Foundry.
Per-user and per-route observability.
Quota-ready architecture.
Log Analytics reporting by identity, client, API, and token usage.

The developer workflow stays simple:

az login
claude

or:

az login
codex

The enterprise control plane becomes APIM.

Final thoughts

Using Azure API Management as an AI Gateway gives organizations a practical way to scale AI coding assistants without scattering secrets, policy, and telemetry across individual machines.

Claude Code and Codex can still feel local and natural for developers. But the enterprise gets centralized authentication, routing, governance, quotas, and observability.

The most important principles are:

Do not distribute backend API keys.
Use Entra tokens to identify users at APIM.
Use APIM managed identity to call Foundry.
Strip client credential headers before forwarding.
Log identity metadata and token usage with CorrelationId.
Apply governance at the gateway, not on individual machines.

For organizations standardizing AI-assisted development, APIM can be much more than an API proxy. It can become the AI Gateway where enterprise identity, governance, and reporting come together.

Jordan Bardwell
Director, Rapid Development & Innovation · Epiq
letsbuild@jordanbardwell.com