Apollo GraphOS Platform
OverviewQuickstart
APIs and Tools
Schema Management
Explorer IDE
Graph Security
Metrics and Insights
Access Management
Graph Management
Production Readiness
Platform Limits

Set Up an OAuth Client

Create an OAuth client and use its tokens in your CI/CD pipelines


PLAN REQUIRED
This feature is available on the following GraphOS plans: Standard, Enterprise.

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:

GraphQL
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:

ArgumentDescription
organizationIdRequired. The ID of the GraphOS organization that owns the client and the graphs you grant it access to.
clientNameRequired. A name that distinguishes this client from others, such as ci-schema-publish. Must be 1 to 100 characters.
scopesRequired. The scopes to grant to this client. Provide at least one.
resourcesRequired. 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.
secretLifetimeDaysOptional. 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:

JSON
Request payload
{
  "organizationId": "<YOUR_ORGANIZATION_ID>",
  "clientName": "ci-schema-publish",
  "scopes": ["rover:cli:write"],
  "resources": {
    "graphs": [
      { "graphId": "<YOUR_GRAPH_ID>" }
    ]
  },
  "secretLifetimeDays": 90
}

Example response:

JSON
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"
      }
    }
  }
}
caution
The 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:

txt
https://auth.apollographql.com/oauth2/token

Send a POST request with grant_type=client_credentials. The recommended approach is to authenticate with HTTP Basic authentication.

Bash
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:

JSON
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:

txt
Authorization: Bearer <ACCESS_TOKEN>

GraphOS also accepts the access token in the x-api-key header:

txt
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:

GraphQL
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:

JSON
Request payload
{
  "organizationId": "<YOUR_ORGANIZATION_ID>",
  "first": 50
}

Example response:

JSON
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.

note
Client secrets are never included when you fetch a client. If you aren't an Org Admin, this query returns an empty list instead of an error.

Fetch an OAuth client by ID

Pass your client's clientId to return that client's details:

GraphQL
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:

JSON
Request payload
{
  "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:

GraphQL
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:

JSON
Request payload
{
  "organizationId": "<YOUR_ORGANIZATION_ID>",
  "clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90",
  "gracePeriodDays": 7
}

Example response:

JSON
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.

caution
Any automation still using an old secret fails its next token request. Existing tokens still work until they expire (15 minutes). If you're rotating on a schedule, set a grace period long enough to update your secret manager and pipelines. If the secret has leaked, keep the default so the old secret stops working immediately.

The API returns the new clientSecret only once. Store it immediately in a secure location.

Delete an OAuth client

caution
Deleting an OAuth client is permanent and can't be undone. The client's secrets stop working immediately, and automation using the client can no longer request tokens. Existing tokens still work until they expire (15 minutes).

Delete an OAuth client using the deleteOAuthClient mutation:

GraphQL
mutation DeleteOAuthClient($organizationId: ID!, $clientId: ID!) {
  organization(id: $organizationId) {
    deleteOAuthClient(clientId: $clientId)
  }
}

Include a request payload with the following properties, using your own values:

JSON
Request payload
{
  "organizationId": "<YOUR_ORGANIZATION_ID>",
  "clientId": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90"
}

Example response for a successful deletion:

JSON
Response
{
  "data": {
    "organization": {
      "deleteOAuthClient": "3f6c2b1e-8d4a-4e0b-9f7a-2c5d1e8b4a90"
    }
  }
}

Deletion also removes the client's secrets and the service account GraphOS created for it.