Using Outbound Apps

For hands-on examples of outbound apps in action, check out our Examples Guide, which includes tool-calling examples for AI agents.

After configuring an Outbound App in Descope and connecting your users to it, you can start using the tokens to access third-party APIs on behalf of your users.

With Outbound Apps, you can fetch the tokens and use them to make authenticated requests to third-party provider APIs, enabling seamless integration without exposing sensitive credentials to your application.

Fetching Outbound Apps Tokens

This section covers how to fetch outbound app tokens for your users and tenants.

To retrieve a user's token for an outbound app, you can use either the REST API or one of our SDKs.

You'll either need your Project ID and Management Key, or a user/tenant scoped Inbound App token. For more information on how to authenticate your requests, see the Authentication for Token Fetching section below.

Authentication for Token Fetching

You can authenticate requests to fetch outbound app tokens using either:

  • A Management Key, formatted as Bearer <PROJECT_ID>:<MANAGEMENT_KEY>
  • An Inbound App token, formatted as Bearer <ACCESS_TOKEN> (from the user's OAuth login)

You should use Management Keys when:

  • Your backend environment is secure and can safely store secrets.
  • You want full administrative control over token access across users and tenants.

Otherwise, you should use Inbound App tokens when:

  • You're operating from a public or frontend-facing client that cannot store secrets securely.
  • You're building an MCP server or tool execution layer and want to use the Access Control Plane to determine—in real time—whether a user or tenant is authorized to retrieve an outbound token.
    • If the access control policy denies access (e.g., the user's role does not permit the requested tool), Descope will block the outbound token request, even if the user has already connected to the provider.

Descope provides two methods for fetching user-level outbound app tokens, depending on whether you know the exact scopes you need:

Currently, only the Java SDK supports fetching outbound app tokens using a Management Key. If you want to fetch tokens using an Inbound App token instead of a Management Key, you'll need to use the Descope REST API directly, as this is not yet supported in the SDK.

import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;
 
// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();
 
// Fetch latest token
FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest();
request.setAppId("google-contacts");
request.setUserId("user-123");
 
FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserTokenLatest(request);
String accessToken = response.getToken().getAccessToken();

Request Parameters

  • appId (required): The ID of the outbound app.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional): The tenant ID of the user, if a user has multiple tokens associated with different tenants.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetch Token with Specific Scopes

Use this method when you need a token with specific scopes. Important: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a 404 Token not found error.

import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;
 
// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();
 
// Fetch token with specific scopes
FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest();
request.setAppId("google-contacts");
request.setUserId("user-123");
request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly"));
 
FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserToken(request);
String accessToken = response.getToken().getAccessToken();

Request Parameters

  • appId (required): The ID of the outbound app.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional, required if no userId): The tenant ID of the user, if a user has multiple tokens associated with different tenants.
  • scopes (required): An array of exact scopes that match the original token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetching Tenant-Level Tokens

In addition to user-specific tokens, you can also fetch tenant-level tokens for outbound apps. These are useful when you need to access APIs on behalf of a tenant rather than a specific user.

Descope provides the same two methods for tenant-level outbound app tokens as user-level tokens. Depending on whether you know the exact scopes of the token you need, you can use the following methods:

This method is recommended when you don't know the exact scopes or want the most recent valid token for the tenant, regardless of scopes.

import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;
 
// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();
 
// Fetch latest tenant token
FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest();
request.setAppId("google-contacts");
request.setTenantId("tenant-123");
 
FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantTokenLatest(request);
String accessToken = response.getToken().getAccessToken();

Latest Tenant Token Request Parameters

The tenant-level APIs use a similar request schema:

  • appId (required): The ID of the outbound app.
  • tenantId (optional, required if no userId): The tenant ID if you're fetching a tenant-level token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetch Tenant Token with Specific Scopes

Use this method when you need a tenant token with specific scopes. Important: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a 404 Token not found error.

import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;
 
// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();
 
// Fetch tenant token with specific scopes
FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest();
request.setAppId("google-contacts");
request.setTenantId("tenant-123");
request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly"));
 
FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantToken(request);
String accessToken = response.getToken().getAccessToken();

Specific Tenant Token Request Parameters

The tenant-level APIs use a similar request schema:

  • appId (required): The ID of the outbound app.
  • tenantId (optional, required if no userId): The tenant ID if you're fetching a tenant-level token.
  • scopes (required): An array of exact scopes that match the original token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Outbound App Token Response

The refresh_token will not be returned unless withRefreshToken is set to true in the request.

The response will include the user/tenant token details, similar to the example below:

{
  "token": {
    "id": "xxxx",
    "appId": "google-contacts",
    "userId": "xxxx",
    "tokenSub": "",
    "accessToken": "ya29.xxxx",
    "accessTokenType": "Bearer",
    "accessTokenExpiry": "1741107113",
    "hasRefreshToken": true,
    "refreshToken": "xxxx",
    "lastRefreshTime": "1741103514",
    "lastRefreshError": "",
    "scopes": [
      "https://www.googleapis.com/auth/contacts.readonly"
    ]
  }
}

Using Tokens with Third-Party APIs

Once you have the access token, you can use it to make authenticated requests to the third-party provider's API.

Example: Google Contacts API

The following example shows how to fetch a user's contacts using the Google Contacts API with a token obtained from an outbound app.

import json
import requests
 
access_token = "ya29.xxxx"
user_mail = "user@example.com"
request_url = "https://people.googleapis.com/v1/people/me/connections"
params = {
    'personFields': 'names,emailAddresses',  # Specify valid fields
    'pageSize': 100  # Adjust as needed
}
headers = {"Authorization": f"Bearer {access_token}"}
 
response = requests.get(request_url, headers=headers, params=params)
 
# Check the response status code
if response.status_code == 200:
    try:
        # Parse the JSON response
        response_array = response.json()
        if 'connections' in response_array and response_array['connections']:
            # Pretty print the JSON response
            print('Response:', json.dumps(response_array['connections'], indent=4))
        else:
            print('No contacts found.')
    except requests.exceptions.JSONDecodeError:
        print('Error decoding JSON:', response.text)
else:
    print(f'Error: {response.status_code}')
    print('Response Text:', response.text)

For more detailed examples and AI agent implementations, see our Examples Guide.

Token Management Best Practices

Error Handling

When working with outbound app tokens, you may encounter different types of errors. Here's what each error code means and how to handle them:

Common Error Codes

Status CodeMeaningCommon Causes
401UnauthorizedInvalid management key or project ID
403ForbiddenInsufficient permissions or invalid tenant access
404Token not foundUser never connected to the app, token was cleared, or wrong scopes provided
500Server errorInvalid HTTP method (not POST) or malformed JSON payload

Error Handling Example

import requests
from requests.exceptions import RequestException
 
def fetch_outbound_token(app_id, user_id, scopes=None):
    """Fetch an outbound app token with proper error handling."""
    headers = {"Authorization": f"Bearer {PROJECT_ID}:{MANAGEMENT_KEY}"}
    
    try:
        if scopes:
            # Use specific scopes endpoint
            url = "https://api.descope.com/v1/mgmt/outbound/app/user/token"
            data = {"appId": app_id, "userId": user_id, "scopes": scopes}
        else:
            # Use latest token endpoint
            url = "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest"
            data = {"appId": app_id, "userId": user_id}
        
        response = requests.post(url, headers=headers, json=data, timeout=30)
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.HTTPError as e:
        if response.status_code == 404:
            # Token not found - either never existed or was cleared recently
            print("Token not found. The user may not have connected to this app, "
                  "or the token may have been cleared.")
            return None
        elif response.status_code == 500:
            # Server error - issue with HTTP request method or JSON payload
            print("Server error. Check your request method (should be POST) "
                  "and ensure your JSON payload is properly formatted.")
            return None
        else:
            print(f"HTTP error occurred: {e}")
            return None
    except requests.exceptions.Timeout:
        print("Request timed out")
        return None
    except RequestException as e:
        print(f"Request failed: {e}")
        return None
 
def make_api_request(access_token, url, params=None):
    """Make a request to a third-party API with proper error handling."""
    headers = {"Authorization": f"Bearer {access_token}"}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as e:
        if response.status_code == 401:
            print("Access token may be invalid or expired")
        elif response.status_code == 403:
            print("Insufficient permissions for this request")
        else:
            print(f"HTTP error occurred: {e}")
        return None
    except requests.exceptions.Timeout:
        print("Request timed out")
        return None
    except RequestException as e:
        print(f"Request failed: {e}")
        return None

Scope Management

Outbound apps can store multiple tokens for the same user, each with different scopes.

When connecting to outbound apps, always request the minimum number of scopes necessary for your use case.

Granting only the least privileges possible to your application or agent helps reduce security risks if a token is ever compromised.

By limiting access, you minimize the potential impact of a breach and follow the principle of least privilege, which is a security best practice for all integrations and automations.

Was this helpful?