On Behalf Of Requests
On Behalf Of (OBO) lets a single service account make API calls as one of their end users. The service account authenticates once, requests an additional access token that is a short-lived OBO token scoped to a specific user, then uses that token on every downstream request. The call runs against the target user's entitlements, and all activity (conversation history, searches, audit logs, and credit consumption) is attributed to that user.
Use OBO when one integration serves many AlphaSense users and each request must reflect the calling user's permissions, content access, and usage.
How the flow works
OBO is a multiple-step flow on top of the standard auth flow:
- Authenticate as your service account. Run the normal password grant to obtain a service access token. See Authentication.
- Request an OBO token. Call the
onBehalfOfAccessTokenGraphQL query with the target user'sid. The response is a new access token scoped to that user. - Call the API as the target user. Use the OBO token as the Bearer token on every downstream request for that user — GenSearch, Workflow Agents, Document Search, etc.
A single service account can hold many active OBO tokens concurrently, one per target user (cache
tokens by targetUserId to avoid unnecessary tokens). Switch between users by changing the
Authorization header on each request.
Prerequisites
OBO must be configured by your AlphaSense account team before your service account can request OBO tokens. Coordinate with your Account team to enable:
| Requirement | Description |
|---|---|
| Feature flag | A specific permission flag on the service account. Until enabled, onBehalfOfAccessToken returns an authorization error. |
| Admin role | The service account is treated as an admin and can request OBO tokens for any active user at your company. |
| IP allowlist | The IP range(s) from which OBO requests may originate. Initially the allowlist is open; you provide ranges to narrow impersonation access. |
Provide the IP range(s) from which your service will make API requests using OBO tokens. This typically includes your service's deployment infrastructure. The allowlist applies to every request made with an OBO token, so it must cover every machine that will issue OBO-scoped calls.
onBehalfOfAccessToken returns an authorization error until the feature flag is enabled on your
service account. Contact support@alpha-sense.com to begin setup.
Authenticate as the service account
Authenticate exactly as documented in Authentication. Use the credentials of your service account. The response includes an access token that you use in the next step to request OBO tokens.
export ALPHASENSE_API_KEY="your-service-api-key"
export ALPHASENSE_CLIENT_ID="your-service-client-id"
export ALPHASENSE_CLIENT_SECRET="your-service-client-secret"
export ALPHASENSE_EMAIL="service-account@example.com"
export ALPHASENSE_PASSWORD="your-service-password"
Look up your target users
If you already know the id of the user you want to act on behalf of, skip this step. Otherwise,
use companyUsers to list every user at your company. Page through results with limit and
offset.
query CompanyUsers($limit: Int, $offset: Int) {
companyUsers(limit: $limit, offset: $offset) {
total
users {
id
isActive
email
username
}
}
}
- Python
- JavaScript
- cURL
import csv
import os
import requests
GRAPHQL_URL = "https://api.alpha-sense.com/gql"
headers = {
"x-api-key": os.environ["ALPHASENSE_API_KEY"],
"clientid": os.environ["ALPHASENSE_CLIENT_ID"],
"Authorization": f"Bearer {service_access_token}",
"Content-Type": "application/json",
}
query = """
query CompanyUsers($limit: Int, $offset: Int) {
companyUsers(limit: $limit, offset: $offset) {
total
users {
id
isActive
email
username
}
}
}
"""
with open("company_users.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["id", "email", "username", "isActive"])
offset = 0
while True:
response = requests.post(
GRAPHQL_URL,
headers=headers,
json={"query": query, "variables": {"limit": 100, "offset": offset}},
)
response.raise_for_status()
result = response.json()["data"]["companyUsers"]
for user in result["users"]:
writer.writerow([user["id"], user["email"], user["username"], user["isActive"]])
offset += len(result["users"])
if offset >= result["total"] or not result["users"]:
break
const GRAPHQL_URL = 'https://api.alpha-sense.com/gql'
const headers = {
'x-api-key': process.env.ALPHASENSE_API_KEY,
clientid: process.env.ALPHASENSE_CLIENT_ID,
Authorization: `Bearer ${serviceAccessToken}`,
'Content-Type': 'application/json',
}
const query = `
query CompanyUsers($limit: Int, $offset: Int) {
companyUsers(limit: $limit, offset: $offset) {
total
users {
id
isActive
email
username
}
}
}
`
const allUsers = []
let offset = 0
while (true) {
const response = await fetch(GRAPHQL_URL, {
method: 'POST',
headers,
body: JSON.stringify({query, variables: {limit: 100, offset}}),
})
if (!response.ok) {
throw new Error(`companyUsers failed (${response.status})`)
}
const {
data: {companyUsers},
} = await response.json()
allUsers.push(...companyUsers.users)
offset += companyUsers.users.length
if (offset >= companyUsers.total || companyUsers.users.length === 0) break
}
console.log(`Found ${allUsers.length} users`)
curl -X POST "https://api.alpha-sense.com/gql" \
-H "x-api-key: ${ALPHASENSE_API_KEY}" \
-H "clientid: ${ALPHASENSE_CLIENT_ID}" \
-H "Authorization: Bearer ${SERVICE_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"query": "query CompanyUsers($limit: Int, $offset: Int) { companyUsers(limit: $limit, offset: $offset) { total users { id isActive email username } } }",
"variables": { "limit": 100, "offset": 0 }
}'
| Field | Type | Description |
|---|---|---|
total | integer | Total number of users at your company. |
users[].id | integer | The user identifier — pass this as targetUserId. |
users[].email | string | The user's email address. |
users[].username | string | The user's username. |
users[].isActive | boolean | Whether the user is active. OBO tokens cannot be minted for inactive users. |
companyUsers also accepts a query argument that prefix-matches against username, firstName,
lastName, and email. When you already know which users you'll act on behalf of, prefer query
over paging — one call instead of N pages:
query CompanyUsers($query: String) {
companyUsers(query: $query) {
total
users {
id
email
username
isActive
}
}
}
Pass {"query": "alice@example.com"} to find a single user, or {"query": "alice"} to match every
user whose username, name, or email starts with alice.
Request an OBO token
Call onBehalfOfAccessToken with the target user's id. The response contains an access token
scoped to that user.
query OnBehalfOfAccessToken($targetUserId: Int!) {
onBehalfOfAccessToken(targetUserId: $targetUserId) {
accessToken
expiresIn
scope
tokenType
}
}
| Variable | Type | Required | Description |
|---|---|---|---|
targetUserId | Int | Yes | The id of the user to act on behalf of (from companyUsers). |
The request itself uses the service account's headers — same as any other GraphQL call:
| Header | Value |
|---|---|
x-api-key | Your service account's API key. |
clientid | Your service account's OAuth2 client ID. |
Authorization | Bearer <service_access_token>. |
Content-Type | application/json |
- Python
- JavaScript
- cURL
query = """
query OnBehalfOfAccessToken($targetUserId: Int!) {
onBehalfOfAccessToken(targetUserId: $targetUserId) {
accessToken
expiresIn
scope
tokenType
}
}
"""
response = requests.post(
"https://api.alpha-sense.com/gql",
headers={
"x-api-key": os.environ["ALPHASENSE_API_KEY"],
"clientid": os.environ["ALPHASENSE_CLIENT_ID"],
"Authorization": f"Bearer {service_access_token}",
"Content-Type": "application/json",
},
json={"query": query, "variables": {"targetUserId": 123456}},
)
response.raise_for_status()
obo_token = response.json()["data"]["onBehalfOfAccessToken"]["accessToken"]
const query = `
query OnBehalfOfAccessToken($targetUserId: Int!) {
onBehalfOfAccessToken(targetUserId: $targetUserId) {
accessToken
expiresIn
scope
tokenType
}
}
`
const response = await fetch('https://api.alpha-sense.com/gql', {
method: 'POST',
headers: {
'x-api-key': process.env.ALPHASENSE_API_KEY,
clientid: process.env.ALPHASENSE_CLIENT_ID,
Authorization: `Bearer ${serviceAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({query, variables: {targetUserId: 123456}}),
})
const oboToken = (await response.json()).data.onBehalfOfAccessToken.accessToken
curl -X POST "https://api.alpha-sense.com/gql" \
-H "x-api-key: ${ALPHASENSE_API_KEY}" \
-H "clientid: ${ALPHASENSE_CLIENT_ID}" \
-H "Authorization: Bearer ${SERVICE_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"query": "query OnBehalfOfAccessToken($targetUserId: Int!) { onBehalfOfAccessToken(targetUserId: $targetUserId) { accessToken expiresIn scope tokenType } }",
"variables": { "targetUserId": 123456 }
}'
Response
{
"data": {
"onBehalfOfAccessToken": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...",
"expiresIn": 1800,
"scope": "<scopes granted by AlphaSense>",
"tokenType": "Bearer"
}
}
}
| Field | Type | Description |
|---|---|---|
accessToken | string | The OBO access token. Use as a Bearer token on all downstream requests. |
expiresIn | integer | Token lifetime in seconds. 1800 (30 minutes). |
scope | string | Scopes granted to the token. |
tokenType | string | Always "Bearer". |
Call the API as the target user
Use the OBO token like a normal access token. The only thing that changes from your service account
flow is the value of the Authorization header.
| Header | Value |
|---|---|
x-api-key | Your service account's API key (unchanged). |
clientid | Your service account's client ID (unchanged). |
Authorization | Bearer <obo_access_token> — replaces the service token. |
Content-Type | application/json |
Every GenSearch query, Workflow Agent execution, or Document Search made with these headers runs as the target user.
- Python
- JavaScript
- cURL
obo_headers = {
"x-api-key": os.environ["ALPHASENSE_API_KEY"],
"clientid": os.environ["ALPHASENSE_CLIENT_ID"],
"Authorization": f"Bearer {obo_token}",
"Content-Type": "application/json",
}
response = requests.post(
"https://api.alpha-sense.com/gql",
headers=obo_headers,
json={
"query": "mutation GenSearch($input: GenSearchInput!) { genSearch { auto(input: $input) { id } } }",
"variables": {"input": {"prompt": "What were Apple's key Q4 2024 financial results?"}},
},
)
const oboHeaders = {
'x-api-key': process.env.ALPHASENSE_API_KEY,
clientid: process.env.ALPHASENSE_CLIENT_ID,
Authorization: `Bearer ${oboToken}`,
'Content-Type': 'application/json',
}
const response = await fetch('https://api.alpha-sense.com/gql', {
method: 'POST',
headers: oboHeaders,
body: JSON.stringify({
query: `mutation GenSearch($input: GenSearchInput!) {
genSearch { auto(input: $input) { id } }
}`,
variables: {input: {prompt: "What were Apple's key Q4 2024 financial results?"}},
}),
})
curl -X POST "https://api.alpha-sense.com/gql" \
-H "x-api-key: ${ALPHASENSE_API_KEY}" \
-H "clientid: ${ALPHASENSE_CLIENT_ID}" \
-H "Authorization: Bearer ${OBO_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation GenSearch($input: GenSearchInput!) { genSearch { auto(input: $input) { id } } }",
"variables": { "input": { "prompt": "What were Apple Q4 2024 financial results?" } }
}'
Token lifecycle
- OBO tokens are valid for 30 minutes from issuance.
- They are not refreshable. To continue acting on behalf of a user past expiry, call
onBehalfOfAccessTokenagain with the sametargetUserId. - A single service account can hold many active OBO tokens at once, one per target user. Cache
tokens by
targetUserIdalongside an expiry timestamp. - Tokens cannot be requested for inactive users (
isActive: false). - The OBO token's lifetime is independent of the service account's access token. Refreshing or rotating the service token does not invalidate outstanding OBO tokens, and an expired service token does not void an unexpired OBO token. You only need a valid service token when minting a new OBO token.
Attribution
All activity performed with an OBO token is attributed to the target user, not the service account:
- Conversation history appears in the target user's GenSearch threads.
- Searches and Workflow Agent runs appear in the target user's recent activity.
- Credit consumption is billed against the target user's entitlement.
- Audit logs record the target user as the actor, and also the service account as the impersonator.
Complete end-to-end example
The script below stitches the four steps together: authenticate the service account, mint an OBO token for a target user, and run a GenSearch query as that user.
#!/usr/bin/env python3
"""AlphaSense Agent API — On Behalf Of quickstart."""
import os
import requests
AUTH_URL = "https://api.alpha-sense.com/auth"
GRAPHQL_URL = "https://api.alpha-sense.com/gql"
API_KEY = os.environ["ALPHASENSE_API_KEY"]
CLIENT_ID = os.environ["ALPHASENSE_CLIENT_ID"]
CLIENT_SECRET = os.environ["ALPHASENSE_CLIENT_SECRET"]
EMAIL = os.environ["ALPHASENSE_EMAIL"]
PASSWORD = os.environ["ALPHASENSE_PASSWORD"]
TARGET_USER_ID = int(os.environ["ALPHASENSE_TARGET_USER_ID"])
# Authenticate as the service account
auth = requests.post(
AUTH_URL,
headers={
"x-api-key": API_KEY,
"Content-Type": "application/x-www-form-urlencoded",
},
data={
"grant_type": "password",
"username": EMAIL,
"password": PASSWORD,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
)
auth.raise_for_status()
service_token = auth.json()["access_token"]
print("Service account authenticated.")
# Exchange for an OBO token
service_headers = {
"x-api-key": API_KEY,
"clientid": CLIENT_ID,
"Authorization": f"Bearer {service_token}",
"Content-Type": "application/json",
}
obo_query = """
query OnBehalfOfAccessToken($targetUserId: Int!) {
onBehalfOfAccessToken(targetUserId: $targetUserId) {
accessToken
expiresIn
}
}
"""
obo_response = requests.post(
GRAPHQL_URL,
headers=service_headers,
json={"query": obo_query, "variables": {"targetUserId": TARGET_USER_ID}},
)
obo_response.raise_for_status()
obo_token = obo_response.json()["data"]["onBehalfOfAccessToken"]["accessToken"]
print(f"OBO token obtained for user {TARGET_USER_ID}.")
# Call GenSearch as the target user
obo_headers = {**service_headers, "Authorization": f"Bearer {obo_token}"}
search = requests.post(
GRAPHQL_URL,
headers=obo_headers,
json={
"query": "mutation GenSearch($input: GenSearchInput!) { genSearch { auto(input: $input) { id } } }",
"variables": {"input": {"prompt": "What were Apple's key Q4 2024 financial results?"}},
},
)
search.raise_for_status()
print("GenSearch started as target user:", search.json()["data"]["genSearch"]["auto"]["id"])
Error handling
| Error | Cause | Resolution |
|---|---|---|
401 Unauthorized | Service token or OBO token is invalid or expired. | Re-authenticate / re-mint. |
403 Forbidden (FORBIDDEN) | Service account lacks OBO permission, or the request IP is not in the configured allowlist. | Contact support@alpha-sense.com to enable OBO or update the IP allowlist. |
403 Forbidden (OBO_OPERATION_NOT_ALLOWED) | The requested GraphQL operation is not on the OBO allowlist for the calling environment. | Confirm the operation is OBO-eligible; contact support if your use case requires additional operations to be allowlisted. |
targetUserId not found / inactive | The targetUserId does not exist at your company, or the user is inactive. | Look up valid IDs with companyUsers and confirm isActive: true. |
429 Too Many Requests | Too many OBO mint requests in a short window. This only happens if you surpass your normal rate limit. | Cache OBO tokens for their full 30-minute lifetime; back off and retry. |
- Authentication — the base password-grant flow that OBO builds on.
- Quick Start — end-to-end GenSearch flow you can run with either a service token or an OBO token.
- Credits & Rate Limits — credit costs are attributed to the user whose token is on the request.