EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/docs/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
Set Up an OAuth Client
Create an OAuth client and use its tokens in your CI/CD pipelines
This guide walks through creating a GraphOS OAuth client, requesting an access token, and managing the client over its lifetime.
Prerequisites
Before you begin, review these requirements:
You need the GraphOS Org Admin role. Only Org Admins can create, list, rotate, and delete OAuth clients.
Your organization needs a Standard or Enterprise plan.
You need a personal API key to authenticate your requests to the Platform API.
You need your organization ID and the ID of each graph the client needs to access. You can find your organization ID on the Settings page of your organization in GraphOS Studio.
Set up your OAuth client
Step 1: Create an OAuth client
Create an OAuth client using the createOAuthClient mutation from the Platform API:
mutation CreateOAuthClient(
$organizationId: ID!
$clientName: String!
$scopes: [String!]!
$resources: OAuthClientResourceInput!
$secretLifetimeDays: Int
) {
organization(id: $organizationId) {
createOAuthClient(
clientName: $clientName
scopes: $scopes
resources: $resources
secretLifetimeDays: $secretLifetimeDays
) {
clientId
clientName
clientSecret
secretExpiresAt
grantTypes
scopes
resources {
resourceType
resourceId
}
createdAt
}
}
}The mutation accepts the following arguments:
| Argument | Description |
|---|---|
organizationId | Required. The ID of the GraphOS organization that owns the client and the graphs you grant it access to. |
clientName | Required. A name that distinguishes this client from others, such as ci-schema-publish. Must be 1 to 100 characters. |
scopes | Required. The scopes to grant to this client. Provide at least one. |
resources | Required. The graphs to grant this client access to. Use the graph ID and format this property as { "graphs": [{ "graphId": "..." }] }. Provide between 1 and 50 graphs. |
secretLifetimeDays | Optional. The number of days until the returned secret expires, between 1 and 730. Defaults to 730 days (or two years). The system rejects requests outside this range. |
Include a request payload with the following properties, using your own values:
{
"organizationId": "<YOUR_ORGANIZATION_ID>",
"clientName": "ci-schema-publish",
"scopes": ["rover:cli:write"],
"resources": {
"graphs": [
{ "graphId": "<YOUR_GRAPH_ID>" }
]
},
"secretLifetimeDays": 90
}Example response:
{
"data": {
"organization": {
"createOAuthClient": {
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90",
"clientName": "ci-schema-publish",
"clientSecret": "<CLIENT_SECRET>",
"secretExpiresAt": "2026-12-29T16:40:17.876252636Z",
"grantTypes": ["CLIENT_CREDENTIALS"],
"scopes": ["rover:cli:write"],
"resources": [
{ "resourceType": "GRAPH", "resourceId": "test-graph-id" },
{ "resourceType": "ORGANIZATION", "resourceId": "test-org-id" }
],
"createdAt": "2026-09-30T16:40:17.992591000Z"
}
}
}
}clientSecret is returned only once, in the response to this mutation. You can't retrieve it later. Copy it immediately and store it in your secret manager. If you lose it, rotate the secret or create a new client.The resources list in the response includes your organization as well as the graphs you selected. The organization entry grants read access that the client needs to reach its graphs. It doesn't give the client access to other graphs in your organization.
Step 2: Request an access token
Exchange your client ID and secret for an access token at the GraphOS token endpoint:
https://auth.apollographql.com/oauth2/tokenSend a POST request with grant_type=client_credentials. The recommended approach is to authenticate with HTTP Basic authentication.
curl -X POST https://auth.apollographql.com/oauth2/token \
-u "$APOLLO_OAUTH_CLIENT_ID:$APOLLO_OAUTH_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials"Example response:
{
"access_token": "<ACCESS_TOKEN>",
"token_type": "Bearer",
"expires_in": 900,
"scope": "rover:cli:write"
}The token endpoint accepts an optional scope parameter, but it doesn't change what the token can do. An access token always has the permissions of the scopes the client was created with, so choose the narrowest scopes when you create the client.
Access tokens expire after 15 minutes, as shown in the expires_in property of the response. Request a new token at the start of each job, or whenever the current one expires. The client_credentials grant doesn't issue refresh tokens.
Step 3: Use the access token
Send the access token as a bearer token in the Authorization header of your requests:
Authorization: Bearer <ACCESS_TOKEN>GraphOS also accepts the access token in the x-api-key header:
x-api-key: <ACCESS_TOKEN>Manage OAuth clients
Use the Platform API to list, rotate, and delete your OAuth clients.
List all OAuth clients
The following query returns every OAuth client in your organization:
query ListOAuthClients($organizationId: ID!, $first: Int, $after: String) {
organization(id: $organizationId) {
oauthClients(first: $first, after: $after) {
totalCount
nodes {
clientId
clientName
grantTypes
scopes
resources {
resourceType
resourceId
}
createdAt
createdBy {
type
actorId
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}Include a request payload with the following properties, using your own values:
{
"organizationId": "<YOUR_ORGANIZATION_ID>",
"first": 50
}Example response:
{
"data": {
"organization": {
"oauthClients": {
"totalCount": 1,
"nodes": [
{
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90",
"clientName": "ci-schema-publish",
"grantTypes": ["CLIENT_CREDENTIALS"],
"scopes": ["rover:cli:write"],
"resources": [
{ "resourceType": "GRAPH", "resourceId": "test-graph-id" },
{ "resourceType": "ORGANIZATION", "resourceId": "test-org-id" }
],
"createdAt": "2026-09-30T16:40:17.992591000Z",
"createdBy": {
"type": "USER",
"actorId": "test-user-id"
}
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": "M2Y2YzJiMWUtOGQ0YS00ZTBiLTlmN2EtMmM1ZDFlOGI0YTkw"
}
}
}
}
}Results are paginated. first defaults to 50, with a maximum of 100. To fetch the next page, pass the endCursor value as after. Pagination is forward-only.
Fetch an OAuth client by ID
Pass your client's clientId to return that client's details:
query FetchOAuthClient($organizationId: ID!, $clientId: ID!) {
organization(id: $organizationId) {
oauthClient(clientId: $clientId) {
clientId
clientName
grantTypes
scopes
resources {
resourceType
resourceId
}
createdAt
}
}
}Include a request payload with the following properties, using your own values:
{
"organizationId": "<YOUR_ORGANIZATION_ID>",
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90"
}The query returns null if no client with that ID exists in your organization, or if you don't have permission to view it.
Rotate a client secret
Rotate your client's secret on a regular schedule, before it expires, or immediately if you suspect it has leaked. To rotate a secret, use the rotateOAuthClientSecret mutation:
mutation RotateOAuthClientSecret(
$organizationId: ID!
$clientId: ID!
$gracePeriodDays: Int
) {
organization(id: $organizationId) {
rotateOAuthClientSecret(
clientId: $clientId
gracePeriodDays: $gracePeriodDays
) {
clientId
clientSecret
secretExpiresAt
}
}
}Include a request payload with the following properties, using your own values:
{
"organizationId": "<YOUR_ORGANIZATION_ID>",
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90",
"gracePeriodDays": 7
}Example response:
{
"data": {
"organization": {
"rotateOAuthClientSecret": {
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90",
"clientSecret": "<NEW_CLIENT_SECRET>",
"secretExpiresAt": "2028-09-29T16:40:17.876252636Z"
}
}
}
}Rotation creates a new secret with the default lifetime of 730 days, and expires every other secret the client has after gracePeriodDays days. gracePeriodDays accepts values 0 to 30 and defaults to 0, which expires the client's existing secrets immediately.
The API returns the new clientSecret only once. Store it immediately in a secure location.
Delete an OAuth client
Delete an OAuth client using the deleteOAuthClient mutation:
mutation DeleteOAuthClient($organizationId: ID!, $clientId: ID!) {
organization(id: $organizationId) {
deleteOAuthClient(clientId: $clientId)
}
}Include a request payload with the following properties, using your own values:
{
"organizationId": "<YOUR_ORGANIZATION_ID>",
"clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90"
}Example response for a successful deletion:
{
"data": {
"organization": {
"deleteOAuthClient": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90"
}
}
}Deletion also removes the client's secrets and the service account GraphOS created for it.