# 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](../security/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.

---

## 🚀 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
    });
});
```

---

## 🔍 Troubleshooting

### "401 Unauthorized"

1. Your app is not listed in the downstream API's `authorized_apps` — see [App Permissions](../security/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/how-authentication-works.md) — flow selection, `ScopeBuilder`, and claim mechanics behind these options
- [App Permissions](../security/configuration/app-permissions.md) — configure the downstream API to accept your calls
- [Kiota Tool Reference](../../reference/tools/kiota.md) — Kiota client generation
- [TypeSpec](typespec.md) — API contract definition
- [Settings and Secrets](settings-and-secrets.md) — configuration management
