> ## Content Index
> Fetch the complete content index at: https://devinside.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# Azure Decision APIs: Root, Deployment, Operation and Token Audience
- URL: https://devinside.cloud/decision-apis-06-azure-two-contracts-one-workflow/
- Published: 2026-11-13T11:04:17.000Z
- Updated: 2026-11-13T22:11:51.000Z
- Description: Microsoft-Decision-1 and Azure OpenAI Decisions use different paths, serializers and token audiences. Bind each to one validated C# profile and keep the token inside the deadline.
- Author: Marcin Borecki
- Tags: Azure & Architecture, AI Engineering, Series: Decision APIs, #series-decision-apis, #dapi06

**Series:** Decision APIs in .NET, part 6 of 10\. Previous: [Part 5, native clients in C#](https://devinside.cloud/decision-apis-05-native-clients-dotnet/). Next: [Part 7, local models and Polish evaluation](https://devinside.cloud/decision-apis-07-local-models-polish-evaluation/).

The serializer from [Part 5](https://devinside.cloud/decision-apis-05-native-clients-dotnet/) passes its tests, the Azure resource answers a ping, and the first real request still fails. Nothing is wrong with the JSON. Someone pasted the Foundry project URL where the resource endpoint belongs, or put the model name where the deployment name belongs, or requested a token for the wrong audience. Each value looked plausible, and each produced a well-formed request the service could not accept.

Azure now offers two decision endpoints with two different contracts: Microsoft-Decision-1 behind a System One-style operation in Microsoft Foundry, and the Azure OpenAI Decisions API behind the OpenAI v1 surface. Both can feed the same typed answers from [Part 3](https://devinside.cloud/decision-apis-03-designing-the-decision-contract/). Their endpoints, serializers, token audiences and roles are not interchangeable. This part makes every one of those identifiers explicit in C#, checks the wiring locally, and is precise about what a local check cannot prove.

- A decision request uses five identifiers: resource root, operation path, deployment name, underlying model and token scope. Each has exactly one place in the request.
- Bind the surface to its operation, serializer and audience in one profile, so a System One body can never be sent to the OpenAI path, and validate the root before any credential is touched.
- Acquire the token inside the same deadline as the HTTP call, from one explicit credential per environment. In production that is the intended managed identity, not a probing chain.
- Deploying, inferring and acting are three permissions. A local test proves the wiring, not that the identity is authorised or the model is good.

## Two Azure surfaces, two contracts

The two endpoints arrived within two weeks of each other and are easy to conflate. They are separate products with separate milestones, not a preview and a GA of the same thing.

|                                   | Microsoft-Decision-1                                                         | Azure OpenAI Decisions                                             |
| --------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Status                            | Public preview, announced 9 October 2026                                     | Generally available, launched 22 October 2026                      |
| Model                             | Microsoft-Decision-1, version 1                                              | gpt-6-luna (the only supported model)                              |
| Operation on the resource         | providers/microsoft/v1/systemone                                             | openai/v1/decisions, no api-version                                |
| Wire format                       | "Similar to" System One: state, questions keyed by name; noul, choice, score | OpenAI Decisions: input, questions array, per-question refusal     |
| Request model                     | Deployment name                                                              | Deployment name                                                    |
| Entra token scope in the docs     | https://cognitiveservices.azure.com/.default                                 | https://ai.azure.com/.default                                      |
| Deployment types                  | GlobalStandard; DataZoneStandard in selected regions                         | Where gpt-6-luna is available; East US 2 excluded from the rollout |
| Launch price, per 1M input tokens | $0.042 (output not charged)                                                  | $0.10, cached $0.01 (output not charged)                           |

Two lines in that table deserve a second look. Microsoft describes the Decision-1 request as similar to a System One API, not identical, and documents three question kinds. Use the Part 5 System One adapter, but test against the Foundry deployment rather than assuming every TypeSafe feature carries over. And the token scopes differ by page: the Decision-1 guide requests a Cognitive Services token, while the Foundry keyless-authentication guide and the Azure OpenAI v1 guide use `https://ai.azure.com/.default`. Follow the page for the surface you call, and treat a real call with the intended identity as the only proof.

## Five identifiers, five places in the request

Most configuration mistakes are an identifier in the wrong slot. Where does each one actually go?

Architecture

### Each identifier has one place in the exchange, and none of them substitutes for another

HTTP EXCHANGE, MICROSOFT-DECISION-1 Configuration Resource root URL host https://RES.services.ai.azure.com/ Operation URL path /providers/microsoft/v1/systemone Deployment request body "model": "triage-ms" Underlying model response body "model": "microsoft-decision-1" Token scope bearer token audience cognitiveservices.azure.com 

1. Five configuration values, five places in one HTTP exchange with a Microsoft-Decision-1 deployment. The client builds every line on the right from the profile on the left.
2. The resource root selects the Azure resource and nothing else. A Foundry project URL such as `/api/projects/triage` is not a root; appending an operation to it builds a different path.
3. The operation selects the API on that resource. It decides the wire format, so the System One path and the OpenAI path cannot share a serializer.
4. The request body names the deployment, the alias you chose when deploying, not the model. A wrong deployment name is a 422 in Microsoft's troubleshooting table.
5. The response names the underlying model, `microsoft-decision-1`. The client must accept that it differs from the deployment name and record it for audit.
6. The token scope sets the audience of the bearer token. It is never a URL the request is posted to, and a Resource Manager token is not a data-plane token.
7. The same five places for Azure OpenAI Decisions: another host, the v1 path without `api-version`, deployment `triage-luna`, model `gpt-6-luna` and the `ai.azure.com` audience.

RES and the deployment names are placeholders. Paths and scopes follow the Microsoft Learn pages for each surface.

## Deploy through the documented surface

For Microsoft-Decision-1 the documented portal route is your Foundry project, then **Model catalog**, **Microsoft-Decision-1** and **Deploy**. Copy the resource endpoint and the deployment name from the deployment page. The equivalent CLI call, for an existing resource, is:

```bash
az cognitiveservices account deployment create \
  --name RESOURCE_NAME \
  --resource-group RESOURCE_GROUP \
  --deployment-name triage-ms \
  --model-name "Microsoft-Decision-1" \
  --model-format Microsoft \
  --model-version "1" \
  --sku-name GlobalStandard \
  --sku-capacity 1
```

That command changes Azure state, needs the Cognitive Services Contributor role or equivalent, and is shown here as an instruction, not as something this article ran. `GlobalStandard` may process inference in any supported region; for locality requirements choose `DataZoneStandard` where it is offered.

For Azure OpenAI Decisions, deploy `gpt-6-luna` on an Azure OpenAI resource in a region where it is available, and choose a deployment name such as `triage-luna`. The Decisions operation lives on the v1 surface, so there is no dated `api-version` query to carry around.

## Bind the profile so wrong combinations cannot be configured

The cheapest defence is to make mismatches unrepresentable. The profile below takes three inputs, the root, the deployment name and the surface, and derives the other three from the surface. Nobody can configure the System One operation with the OpenAI serializer, or the right path with the wrong audience.

```csharp
public enum DecisionSurface { MicrosoftDecision1, AzureOpenAiDecisions }

public sealed record AzureProfile
{
    public Uri Root { get; }
    public string Deployment { get; }
    public DecisionSurface Surface { get; }

    public AzureProfile(Uri root, string deployment, DecisionSurface surface)
    {
        if (!root.IsAbsoluteUri || root.AbsolutePath != "/" || root.Query != "" ||
            root.Fragment != "" || root.UserInfo != "")
            throw new ArgumentException("Root must be the resource endpoint, with no path, query or credentials.");
        if (root.Scheme != Uri.UriSchemeHttps && !root.IsLoopback)
            throw new ArgumentException("Remote endpoints must use HTTPS.");
        if (string.IsNullOrWhiteSpace(deployment))
            throw new ArgumentException("Deployment name is required.");
        (Root, Deployment, Surface) = (root, deployment, surface);
    }

    public string Operation => Surface switch
    {
        DecisionSurface.MicrosoftDecision1 => "providers/microsoft/v1/systemone",
        _ => "openai/v1/decisions",                     // v1 API: no api-version query
    };

    public string TokenScope => Surface switch
    {
        DecisionSurface.MicrosoftDecision1 => "https://cognitiveservices.azure.com/.default",
        _ => "https://ai.azure.com/.default",
    };

    public INativeWire Wire => Surface switch
    {
        DecisionSurface.MicrosoftDecision1 => new SystemOneWire(Operation),
        _ => new OpenAiDecisionsWire(Operation),
    };
}
```

Validation runs in the constructor, before a credential or a socket is involved. In the lab it rejected a project URL, an already-suffixed `/openai/v1/` root, a root carrying `?api-version=`, plain HTTP to a remote host, credentials embedded in the URL and an empty deployment name. Each of those would otherwise surface much later as a 404 or a 401 that points at the wrong field.

Authentication is a `DelegatingHandler` in front of the Part 5 transport. It asks the credential for a token on every request, with the request's own cancellation token, and attaches it as a bearer header. It never stores the token in configuration or logs.

```csharp
public sealed class BearerTokenHandler(TokenCredential credential, string scope) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        AccessToken token = await credential.GetTokenAsync(
            new TokenRequestContext([scope]), cancellationToken);   // cached inside real credentials
        request.Headers.Authorization = new("Bearer", token.Token);
        return await base.SendAsync(request, cancellationToken);
    }
}

public sealed class AzureDecisionClient(AzureProfile profile, TokenCredential credential,
                                        HttpMessageHandler transport, TimeSpan deadline)
{
    private readonly NativeClient client = new(
        new HttpClient(new BearerTokenHandler(credential, profile.TokenScope) { InnerHandler = transport })
            { Timeout = Timeout.InfiniteTimeSpan },
        profile.Root, profile.Wire, deadline);

    public async Task<NativeResult> SendAsync(DecisionTask task, string input, CancellationToken caller)
    {
        try
        {
            return await client.SendAsync(task, profile.Deployment, input, caller);
        }
        catch (Exception e) when (e is AuthenticationFailedException or CredentialUnavailableException)
        {
            return new(Outcome.Authentication, Error: e.GetType().Name);   // no request was sent
        }
    }
}
```

Asking for a token per request is not wasteful. The Azure Identity credentials keep acquired tokens in an in-memory cache, which is exactly why the guidance is to create one credential instance and reuse it. A new `DefaultAzureCredential` per call throws that cache away and, under load, can draw throttling from Microsoft Entra ID.

## Credential acquisition shares the deadline

Part 5 gave each call one budget covering headers, body and parse. Where does getting a token fit? If the credential is slow, for example a managed identity endpoint that is slow to answer, should the decision call start a fresh full-length deadline afterwards?

Sequence

### The token is acquired inside the call's budget, so a slow credential becomes a Deadline

1. The app calls the decision client with its own token. Before anything is sent, the handler asks the credential for a token for the profile's scope, under the same linked budget.
2. The credential returns an access token. The handler attaches it to this request only; the token is never written to configuration, logs or the result.
3. The request goes to the deployment with the bearer header, the deployment name in `model` and the System One body.
4. The response names the underlying model. The Part 3 validator accepts the answers and the client returns `Ok`.
5. On the next call, a real Azure Identity credential answers from its cache. That is why one long-lived credential instance matters.
6. In the lab a credential that needs 800 ms meets a 300 ms budget. It sees the cancellation, no HTTP request is sent, and the caller gets `Deadline`.

Steps 1 to 4 and 6 were executed with a test credential and an in-memory handler. Step 5 describes the caching documented for Azure Identity credentials.

The consequence is a design choice to make explicitly. Either the budget includes credential acquisition, as here, and a slow identity endpoint shows up as `Deadline`; or the application warms the credential at startup and accepts that the first request after a long idle may be slower. What it should not do is start a new inference deadline after an unbounded token wait without saying so.

## Choose the credential deterministically

Azure's own guidance is direct: use deterministic credentials in production. `DefaultAzureCredential` walks a chain of sources, and a changed environment variable or a developer tool on a build agent can silently select a different identity. Pick one credential per environment and name it in code:

```csharp
public static class Credentials
{
    // Local development: one explicit developer credential, not a probing chain.
    public static TokenCredential Developer() => new AzureCliCredential();

    // Deployed workload: the intended user-assigned identity, nothing else.
    public static TokenCredential Workload(string clientId) =>
        new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId(clientId));
}
```

Both factories compile against Azure.Identity 1.21.0; the lab never calls them, because it must not sign in or touch an account. If a team prefers to keep `DefaultAzureCredential` for convenience, Azure.Identity supports narrowing it with the `AZURE_TOKEN_CREDENTIALS` environment variable, for example to `ManagedIdentityCredential` in production.

Both surfaces also accept an API key in an `api-key` header. For a bounded prototype that can be reasonable. If you choose it, keep the key in the existing secret store, never in the endpoint URL, and do not let a key setting and a bearer setting coexist in one profile. Keyless authentication earns its extra setup when you want access controlled by role assignments and no long-lived application secret to rotate.

## Permission to deploy is not permission to infer, or to act

Creating a deployment is a management operation; calling it is a data-plane operation. They are usually performed by different principals and need different roles.

| Activity                           | Who                           | Role in the documentation                                       |
| ---------------------------------- | ----------------------------- | --------------------------------------------------------------- |
| Deploy Microsoft-Decision-1        | Platform engineer or pipeline | Cognitive Services Contributor                                  |
| Call a Foundry model with Entra ID | The workload identity         | Cognitive Services User, assigned at the Foundry resource scope |
| Call Azure OpenAI v1 with Entra ID | The workload identity         | Cognitive Services OpenAI User                                  |
| Assign those roles                 | Administrator                 | Owner or User Access Administrator                              |

Two operational details cause most "it works for me" reports. A familiar role on the wrong resource authorises nothing, and Microsoft notes that role assignments can take up to five minutes to propagate. Neither is visible in a local test, and decoding a token on your machine does not tell you whether the service will accept it.

There is a third permission Azure does not decide at all. A managed identity that may call the decision model says nothing about whether the support operator may draft a reply, arrange a return or issue a refund. Keep that check in application code, as in [Part 1](https://devinside.cloud/decision-apis-01-small-judgment-real-application/), and do not let successful service authentication turn into business authority. The wider pattern of separate deployment, runtime and operator identities is in [three identities in one Azure service](https://devinside.cloud/deployment-runtime-and-operator-three-identities-in-one-azure-service/).

## Read the status, then the field it implicates

When a call fails, the status narrows the search to a few fields, not to one. Microsoft's troubleshooting table for Decision-1 is a good map; the slider below follows it.

Explorable

### A status narrows the search to a few fields, never to exactly one

WHERE TO LOOK Resource root Operation path Deployment name Question shape Credential and scope Role at resource scope Network access Quota and rate HTTP 400 Configuration retry: no What the client reports 

1. 400: the question type or shape is invalid. Check `state`, `questions`, `type` and `criteria`; an OpenAI-shaped body sent to the systemone path fails here.
2. 401: the credential is missing, invalid or expired, or the token was issued for the wrong audience. Credential acquisition succeeding locally does not rule this out.
3. 403: the identity is known but not allowed. Check the role and the scope it was assigned at. If the resource restricts networks, confirm the caller's network is allowed too.
4. 404: the resource endpoint or the API path is wrong. A project URL used as the root, or a doubled `/openai/v1/`, lands here.
5. 422: the deployment name or a question definition is not supported. The model name in place of the deployment name is the classic cause.
6. 429: the deployment's rate limit was exceeded. This is the only status in the set where a retry with back-off is the right next step.

Status meanings from the Microsoft-Decision-1 troubleshooting table on Microsoft Learn. Network access is an extra check for restricted resources, not a cause listed in that table. Outcomes are the Part 5 client's classification.

Keep the status and the bounded error detail with the outcome, and keep the identifiers that produced the request: root, operation, deployment and scope (never the token). With those five values in the log line, most of these failures are a one-minute fix instead of an afternoon of guessing.

## What a local run establishes, and what it cannot

The lab wires the Part 5 client to both profiles with a test `TokenCredential` and an in-memory `HttpMessageHandler` that records every request and returns authored bodies. There is no network listener and no cloud request. On .NET 10 with Azure.Identity 1.21.0 it printed, in part:

```text
PASS  MicrosoftDecision1: url              https://contoso-triage.services.ai.azure.com/providers/microsoft/v1/systemone
PASS  MicrosoftDecision1: body model       "model": "triage-ms"
PASS  MicrosoftDecision1: outcome          Ok, response model microsoft-decision-1
PASS  MicrosoftDecision1: scope            https://cognitiveservices.azure.com/.default
PASS  AzureOpenAiDecisions: url            https://contoso-triage.openai.azure.com/openai/v1/decisions
PASS  AzureOpenAiDecisions: body shape     input + questions array
PASS  AzureOpenAiDecisions: scope          https://ai.azure.com/.default
PASS  profile project URL                  rejected: Root must be the resource endpoint, with no path, query or credentials.
PASS  profile dated api-version            rejected: Root must be the resource endpoint, with no path, query or credentials.
PASS  slow credential inside budget        Deadline after 300 ms, credential saw cancellation: True, requests sent: 0
PASS  credential failure                   Authentication (AuthenticationFailedException), requests sent: 0
PASS  status 422                           Configuration
PASS  token never in report

31 passed, 0 failed; network listeners: 0; cloud requests: 0
```

That is useful evidence about the client: the right URL, the right body, the right audience, the token attached and never printed, cancellation reaching the credential. It certifies nothing about Azure. The remaining acceptance is short and specific: with an authorised deployment and the intended workload identity, send the frozen task through the selected operation, keep a redacted request and response, and check that the normalised answers pass the Part 3 validator. Only then is it worth comparing model answers on labelled tickets, which is where [Part 4](https://devinside.cloud/decision-apis-04-probabilities-thresholds-and-error-cost/)'s thresholds and [Part 7](https://devinside.cloud/decision-apis-07-local-models-polish-evaluation/)'s evaluation take over.

## Worked example: a configuration that fails four ways

#### Plausible values in the wrong slots

```json
{
  "Endpoint": "https://contoso-triage.services.ai.azure.com/api/projects/triage",
  "Model": "Microsoft-Decision-1",
  "ApiVersion": "2024-10-21",
  "TokenScope": "https://management.azure.com/.default",
  "Credential": "DefaultAzureCredential"
}
```

A project URL as root (404), the model name as deployment (422), a dated version on a surface that has none, a Resource Manager audience (401), and a credential chain that may pick a different identity on the next build agent.

#### Three inputs, everything else derived

```json
{
  "Root": "https://contoso-triage.services.ai.azure.com/",
  "Deployment": "triage-ms",
  "Surface": "MicrosoftDecision1",
  "ManagedIdentityClientId": "<client id of the workload identity>"
}
```

Operation, serializer and audience follow from `Surface`. The root is validated at startup, the credential is one named identity, and the deployment name is the one on the deployment page.

## Pitfalls

- **Treating the two surfaces as versions of one API.** Different operation, different wire format, different audience in the docs.
- **Deriving the scope from the endpoint.** Stripping a path from a URL does not produce an audience. Configure it per surface.
- **A new credential per request.** It defeats token caching and can trigger Entra throttling.
- **Unbounded token waits.** Credential acquisition belongs inside the call's deadline, or explicitly before it.
- **Assuming a role works the moment it is assigned.** Allow for propagation, and check the scope it was assigned at.
- **Adding private networking to fix a 404.** Network rules decide who can reach the resource. They never fix a wrong path, deployment or serializer.
- **Logging the request for diagnosis.** Log the five identifiers and the status, never the authorisation header or key.

## Which surface for which job

| If you need                                                    | Prefer                     | Because                                                                       |
| -------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------- |
| Per-question refusals, image input, the official .NET SDK      | Azure OpenAI Decisions     | Documented refusal answer, up to 128 images, DecisionsClient in OpenAI 2.15.0 |
| Lowest input price for text-only routing, data-zone processing | Microsoft-Decision-1       | $0.042 per 1M input tokens at launch, DataZoneStandard where offered          |
| A GA service for production commitments                        | Azure OpenAI Decisions     | Decision-1 is in public preview                                               |
| Both, behind one routing policy                                | Two profiles, one contract | The Part 3 records and validator are shared; only the profile changes         |

## Checklist

- Each deployment has a profile: root, deployment name and surface; operation, serializer and scope are derived.
- The root is the resource endpoint, validated at startup: no path, query, fragment or embedded credentials, HTTPS only.
- The request `model` is the deployment name, and the response `model` is recorded, not compared.
- One credential instance per process, explicit per environment: developer credential locally, the named managed identity in production.
- Token acquisition runs inside the call's budget, and the token never reaches logs or results.
- Deploy, infer and act are separate permissions held by separate principals.
- A live acceptance call with the intended identity is planned and recorded before model quality is evaluated.

**Series:** Previous: [Part 5, native Decision API clients in C#](https://devinside.cloud/decision-apis-05-native-clients-dotnet/). Next: [Part 7, local models and Polish evaluation](https://devinside.cloud/decision-apis-07-local-models-polish-evaluation/).

## Further reading

- [Deploy and use Microsoft-Decision-1](https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/use-foundry-models-microsoft-decision?ref=devinside.cloud) (Microsoft Learn): operation path, deployment, authentication and troubleshooting.
- [Use the Azure OpenAI Decisions API](https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/decisions?ref=devinside.cloud) (Microsoft Learn): endpoint, limits, C# examples with `DecisionsClient`.
- [Azure OpenAI v1 API lifecycle](https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle?ref=devinside.cloud) (Microsoft Learn): the v1 surface without `api-version`, and its token scope.
- [Configure keyless authentication with Microsoft Entra ID](https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/configure-entra-id?ref=devinside.cloud) (Microsoft Learn): Cognitive Services User and propagation.
- [Authentication best practices with the Azure Identity library for .NET](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/best-practices?ref=devinside.cloud): deterministic credentials and credential reuse.
- [Introducing Microsoft-Decision-1 in Microsoft Foundry](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-microsoft-decision-1-in-microsoft-foundry-for-decision-and-classific/4562742?ref=devinside.cloud) (9 October 2026) and [Azure OpenAI Decisions API generally available](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/azure-openai-decisions-api-now-generally-available-in-microsoft-foundry/4567265?ref=devinside.cloud) (23 October 2026).