Skip to content

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)
  2. Code — configure a Kiota client with the downstream API's project ID and permission names

This guide covers the code side.


🚀 Quick Start

Step 1: Add a KiotaReference

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

<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:

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 below.

Step 3: Use the Client

Inject the client into your endpoint or service:

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

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.

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

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

<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

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

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).

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

ClientCredentialsTokenProvider

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

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

💡 Examples

Example 1: Calling a System API for User Data

// 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

// 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

// 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
    });
});

🔍 Troubleshooting

"401 Unauthorized"

  1. Your app is not listed in the downstream API's authorized_apps — see App Permissions
  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