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

To retrieve a user's token for an outbound app, you'll use Descope's Management API with your Project ID and Management Key.

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

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

curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY>" \
  -d '{
  "appId": "google-contacts",
  "userId": "xxxxx",
  "tenantId": "optional-tenant-id",
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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.

curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/user/token" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY>" \
  -d '{
  "appId": "google-contacts",
  "userId": "xxxxx",
  "tenantId": "optional-tenant-id",
  "scopes": [
    "https://www.googleapis.com/auth/contacts.readonly"
  ],
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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.

curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/tenant/token/latest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY>" \
  -d '{
  "appId": "google-contacts",
  "tenantId": "tenant-123",
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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.

curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/tenant/token" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY>" \
  -d '{
  "appId": "google-contacts",
  "tenantId": "tenant-123",
  "scopes": [
    "https://www.googleapis.com/auth/contacts.readonly"
  ],
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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

Example Error Handling

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

When connecting to outbound apps, ensure you only request the scopes your application/agent actually needs.

# Request minimal scopes for basic operations
basic_scopes = ["https://www.googleapis.com/auth/contacts.readonly"]
 
# Request additional scopes only when needed
extended_scopes = [
    "https://www.googleapis.com/auth/contacts.readonly",
    "https://www.googleapis.com/auth/contacts"
]
Was this helpful?