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:
- Infrastructure — be listed as an authorized app in the downstream API's configuration (see App Permissions)
- 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¶
DefaultAccessTokenProvider (Recommended)¶
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"¶
- Your app is not listed in the downstream API's
authorized_apps— see App Permissions - Scopes in code don't match those granted in infrastructure
- Missing delegation scope — verify
user_impersonation(corp) oruser-groups(ext) is in the downstream'sauthorized_appsentry for your app
"403 Forbidden" After Successful Authentication¶
- Insufficient permissions — review the downstream API's
@useAuthin TypeSpec - Verify your app has the correct permission names in
authorized_apps
Token Not Being Acquired¶
- The downstream API must be deployed before your app can discover it
- Both apps must be in the same environment (test, qa, uat, production)
- Check the downstream API's deployment logs for Terraform errors
📚 Related Documentation¶
- How Authentication Works — flow selection,
ScopeBuilder, and claim mechanics behind these options - App Permissions — configure the downstream API to accept your calls
- Kiota Tool Reference — Kiota client generation
- TypeSpec — API contract definition
- Settings and Secrets — configuration management