---
title: Downstream API Calls
description: Generate and configure authenticated Kiota clients for downstream Forge APIs.
moved_from:
  - guides/development/calling-apis.md
  - reference/tools/kiota.md
---

# Downstream API Calls

Configure your Forge application to call other APIs with proper authentication.

| Property          | Value                                                      |
| ----------------- | ---------------------------------------------------------- |
| **Goal**          | Call a downstream API from your application                |
| **Prerequisites** | Downstream API deployed, your app authorized in its config |
| **Time Estimate** | 15-30 minutes                                              |
| **Difficulty**    | Intermediate                                               |

---

## 📋 Overview

To call another API you need to:

1. **Infrastructure**: be listed as an authorized app in the downstream API's configuration (see [App Permissions](../identity/configuration/app-permissions.md))
2. **Code** — configure a Kiota client with the downstream API's project ID and permission names

This guide covers the code side.

The Forge templates include `SAIF.Platform.Kiota.HttpClientLibrary`. A client requires an OpenAPI document and the .NET SDK version in [Version Compatibility](../../reference/version-compatibility.md). The platform library wires authentication into Kiota; Kiota alone does not choose a Forge token flow. For a simple one-off HTTP call, or an endpoint without an OpenAPI document, use `HttpClient` directly instead of generating a Kiota client.

---

## 🚀 Quick Start

### Step 1: Add a KiotaReference

Add a `<KiotaReference>` to your `.csproj` pointing to the downstream API's OpenAPI spec:

```xml
<ItemGroup>
  <KiotaReference Include="DownstreamClient" OpenApi="https://openapi.saif.com/it-api-sys-downstream/test/openapi.v1.yaml">
    <NamespaceName>YourApp.Clients.Downstream</NamespaceName>
  </KiotaReference>
</ItemGroup>
```

**OpenAPI URL pattern:** `https://openapi.saif.com/{project-id}/{environment}/openapi.v1.yaml`

| Environment | Path    |
| ----------- | ------- |
| Test        | `/test/` |
| Production  | `/prod/` |

### Step 2: Configure the HTTP Client

In `Program.cs`, register the client with the downstream API's project ID and the permission names you need:

```csharp
builder.ConfigureHttpClient<DownstreamApiClient>(
    "it-api-sys-downstream",
    options => options.Scopes = ["Client.Read"]);
```

Use the `Client.*` scope names that the downstream API has defined — the same names for user-delegated and service-to-service calls. See [Permission Names](#permission-names) below.

### Step 3: Use the Client

Inject the client into your endpoint or service:

```csharp
app.MapGet("/data", async (DownstreamApiClient client) =>
{
    var result = await client.Resources.GetAsync();
    return Results.Ok(result);
});
```

The platform handles token acquisition, scope formatting, and provider selection automatically.

---

## 🏷️ Permission Names

`options.Scopes` always contains the downstream API's `Client.*` scope names — whether you call on behalf of a user or as your service. The token provider determines the access pattern, not the scope names.

| What you need | `options.Scopes` | Token provider |
| ------------- | ---------------- | -------------- |
| Call on behalf of a user | `["Client.Read"]` | `DefaultAccessTokenProvider` (default) |
| Call as your service (no user) | `["Client.Read"]` | `ClientCredentialsTokenProvider` |

!!! tip "App.* is granted, never requested"
    `App.*` roles are assigned to users or applications — they are never put in `options.Scopes`. Always use the downstream API's `Client.*` scope names.

!!! note "Entra service-to-service always uses `.default`"
    When calling with `ClientCredentialsTokenProvider` via the Corp (Entra) path, the platform always sends `api://{projectId}-{env}/.default` — your access comes from the `App.*` roles the downstream API granted your app. The `Client.*` values in `options.Scopes` are used for the Okta path. Always configure them — Entra works without them, but Okta will fail without the explicit scope names.

---

## ⚙️ Configuration Options

### ConfigureHttpClient

```csharp
builder.ConfigureHttpClient<TClient>(
    projectId: "it-api-sys-target",
    configure: options =>
    {
        options.Scopes = ["Client.Read", "Client.Write"];
    });
```

### HttpClientConfigurationOptions

| Property | Type | Default | Description |
| -------- | ---- | ------- | ----------- |
| `Scopes` | `string[]` | `[]` | Permission names to request |
| `PrefixScopesWithProjectId` | `bool` | `true` | Prefixes scopes with project ID for the token request |
| `AllowedSchemes` | `string[]` | `["https", "http"]` | Allowed URL schemes for service discovery |
| `DisableDefaultScopes` | `bool` | `false` | When `true`, skips auto-appending `user-groups` (Okta) and `user_impersonation` (Entra) for delegated flows |

### KiotaReference Options

```xml
<KiotaReference Include="ClientName" OpenApi="https://openapi.saif.com/project-id/test/openapi.v1.yaml">
  <NamespaceName>YourApp.Clients.ClientName</NamespaceName>
  <IncludePath>/pets;/pets/{id}#GET</IncludePath>
  <ExcludePath>/admin/**</ExcludePath>
</KiotaReference>
```

| Property | Description |
| -------- | ----------- |
| `Include` | Client class name (required) |
| `OpenApi` | URL or path to OpenAPI specification (required) |
| `NamespaceName` | Namespace for generated code |
| `IncludePath` | Limit generation to specific paths (semicolon-separated) |
| `ExcludePath` | Exclude specific paths from generation |

---

## 🔑 Token Providers

### DefaultAccessTokenProvider (Recommended)

Automatically selects the correct flow based on the incoming token. Use this for all new projects.

```csharp
builder.ConfigureHttpClient<ApiClient>(
    "it-api-sys-downstream",
    options => options.Scopes = ["Client.Read"]);
```

### TokenExchangeAccessTokenProvider

Use when you always want to call on behalf of the current user. OBO failures for app tokens surface as errors (no fallback).

```csharp
builder.ConfigureHttpClient<ApiClient, TokenExchangeAccessTokenProvider>(
    "it-api-sys-downstream",
    options => options.Scopes = ["Client.Read"]);
```

### ClientCredentialsTokenProvider

Use for service-to-service calls with no user context.

```csharp
builder.ConfigureHttpClient<ApiClient, ClientCredentialsTokenProvider>(
    "it-api-sys-datasync",
    options => options.Scopes = ["Client.Read"]);
```

---

## 💡 Examples

### Example 1: Calling a System API for User Data

```csharp
// Program.cs
builder.ConfigureHttpClient<UserProfileClient>(
    "it-api-sys-userprofile",
    options => options.Scopes = ["Client.Read"]);

// Endpoint
app.MapGet("/my-profile", async (UserProfileClient client, ClaimsPrincipal user) =>
{
    var userId = user.FindFirstValue("sub");
    var profile = await client.Users[userId].GetAsync();
    return Results.Ok(profile);
});
```

### Example 2: Background Job Calling an API

```csharp
// Program.cs
builder.ConfigureHttpClient<DataSyncClient, ClientCredentialsTokenProvider>(
    "it-api-sys-datasync",
    options => options.Scopes = ["Client.Read", "Client.Write"]);

// Background service
public class SyncService(DataSyncClient client) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var data = await client.Sync.GetAsync();
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Example 3: Aggregating Multiple Downstream APIs

```csharp
// Program.cs
builder
    .ConfigureHttpClient<ClaimsClient>(
        "it-api-sys-claims",
        options => options.Scopes = ["Client.Read"])
    .ConfigureHttpClient<PolicyClient>(
        "it-api-sys-policy",
        options => options.Scopes = ["Client.Read"])
    .ConfigureHttpClient<CustomerClient>(
        "it-api-sys-customer",
        options => options.Scopes = ["Client.Read"]);

// Endpoint
app.MapGet("/dashboard/{customerId}", async (
    string customerId,
    ClaimsClient claimsClient,
    PolicyClient policyClient,
    CustomerClient customerClient) =>
{
    var customerTask = customerClient.Customers[customerId].GetAsync();
    var claimsTask = claimsClient.Claims.GetAsync(q => q.QueryParameters.CustomerId = customerId);
    var policiesTask = policyClient.Policies.GetAsync(q => q.QueryParameters.CustomerId = customerId);

    await Task.WhenAll(customerTask, claimsTask, policiesTask);

    return Results.Ok(new
    {
        Customer = customerTask.Result,
        Claims = claimsTask.Result,
        Policies = policiesTask.Result
    });
});
```

---

## Version notes

### Kiota 1.32.0+ and numeric default initialization

Kiota 1.32.0 and later initialize numeric and Boolean properties from their OpenAPI default values. APIs whose published OpenAPI document was generated with `@saif/platform-typespec` earlier than 3.8.0 can produce clients that fail with `CS0103` for a missing `Status` property.

For the full symptom, cause, and fix, see [Kiota-generated client fails with CS0103 for Status](../../troubleshoot/kiota-generated-client-status-does-not-exist.md).

### Duplicate properties are dropped from generated clients

With Kiota 1.35.0, declaring `type` on both `ProblemDetails` and a concrete error model causes the generated C# error to lose its `Type` property when Kiota replaces the base with `ApiException`. Unlike the `status` defect above, this missing property does not itself cause a build error. Documents generated with `@saif/platform-typespec` 3.8.0 through 3.9.2 contain this duplication in the platform error models.

The fix keeps `type` on `ProblemDetails` and omits it from the concrete models' property spreads. [Release 3.9.3](../../release-notes/3.9.3.md) contains this correction; releases through 3.9.2 do not. Update the API provider to a release containing the fix, republish its OpenAPI document, and regenerate the consuming client. Verify that `type` appears only on the base schema, that concrete error schemas still reference it through `allOf`, and that the generated errors expose both `Type` and `Status`.

Custom error models must therefore omit `type` from the `ProblemDetailsProperties` spread. See [Building custom status models](../../reference/platform-typespec.md#building-custom-status-models), including the C# server-emitter compatibility warning: preserving the hierarchy changes generated server constructors and their response bodies.

## Service discovery: local to deployed

Forge's `AddServiceDefaults()` registers service discovery for HTTP clients. Locally, logical names come from your AppHost resources and `WithReference()` connections; see [Golden Path: Service Discovery](../../reference/architecture/golden-path.md#service-discovery).

### Name mapping when deployed

Deployed resolution reads Azure App Configuration, not an APIM backend lookup. When service discovery is enabled, [`saif-resources/modules/api/service_discovery.tf`](https://github.com/saif-corp/forge/blob/main/src/terraform/saif-resources/modules/api/service_discovery.tf) writes environment-labeled `services:{projectId}:https` and `services:{projectId}:path` keys for the Front Door hostname and APIM path. [`AddAzureDefaults()`](https://github.com/saif-corp/forge/blob/main/src/dotnet/SAIF.Platform.Azure/Extensions.cs) loads `services:*` into application configuration. The client keeps the same logical project ID, but resolves the deployed endpoint rather than a local AppHost port.

The APIM backend identifier remains a separate routing configuration. Finding a service-discovery entry does not prove APIM has a working backend or that the network and identity configuration permit the request.

### Per-topology behavior

- **Private network access:** Forge's [private endpoint decision](../../reference/decisions/0003-all-non-api-management-azure-resources-will-have-private-endpoints.md) distinguishes APIM's ingress from private application resources. A connection failure can indicate network reachability rather than a bad logical name.
- **API versus frontend routing:** API calls go through APIM; frontend traffic goes from Front Door to App Service without APIM. The [PR slot activation guide](../deploy/pr-slot-deployments.md#step-5-activate-the-slot) explains the different cookies and headers.
- **PR slots:** a slot does not require a different logical service name. The `x-saif-slot-routing` map and `SlotRoutingDelegatingHandler` select and propagate slot routing. `AddJwtBearerServices()`, `AddOpenIdConnectServices()`, or `AddSlotRouting()` register that propagation; see [PR Slot Deployments](../deploy/pr-slot-deployments.md) for usage and the [routing design](https://github.com/saif-corp/forge/blob/main/docs/_internal/pr-slot-routing.md) for backend mechanics.
- **Identity boundaries:** a token-exchange failure is not a service-discovery failure. See [How Authentication Works](../../reference/architecture/how-authentication-works.md) and [ADR 0006](../../reference/decisions/0006-okta-multi-tenant-architecture.md) for provider selection and tenant boundaries.

### Troubleshooting deployed failures

When a call works locally but fails after deployment, check in order: the `services:{projectId}:*` keys exist with the target environment label, the APIM backend exists for the same service and environment, the caller can reach any private endpoints, the `x-saif-slot-routing` map includes the target project ID, and the caller and API use the intended Okta organization. Separate a connection error from a 401 or 403 before changing authentication.

## 🔍 Troubleshooting

### "401 Unauthorized"

1. Your app is not listed in the downstream API's `authorized_apps`; see [App Permissions](../identity/configuration/app-permissions.md).
2. Scopes in code don't match those granted in infrastructure
3. Missing delegation scope — verify `user_impersonation` (corp) or `user-groups` (ext) is in the downstream's `authorized_apps` entry for your app

### "403 Forbidden" After Successful Authentication

1. Insufficient permissions — review the downstream API's `@useAuth` in TypeSpec
2. Verify your app has the correct permission names in `authorized_apps`

### Token Not Being Acquired

1. The downstream API must be deployed before your app can discover it
2. Both apps must be in the same environment (test, qa, uat, production)
3. Check the downstream API's deployment logs for Terraform errors

---

## 📚 Related Documentation

- [How Authentication Works](../../reference/architecture/how-authentication-works.md): flow selection, `ScopeBuilder`, and claim mechanics behind these options
- [App Permissions](../identity/configuration/app-permissions.md): configure the downstream API to accept your calls
- [Kiota documentation](https://learn.microsoft.com/en-us/openapi/kiota/overview) — upstream client generation guidance
- [TypeSpec](typespec.md) — API contract definition
- [Settings and Secrets](../config/settings-and-secrets.md): configuration management
