---
title: OAuth 2.0 Scoped Grants
description: Authorize custom integrations to obtain a Storyblok Management API token with fine-grained, multi-space permissions using OAuth 2.0.
url: https://www.storyblok.com/docs/concepts/oauth-scoped-grants
---

# OAuth 2.0 Scoped Grants

OAuth 2.0 scoped grants is an authorization flow that lets a custom integration obtain a [Management API](/docs/api/management) access token. A single authorization limits the token to the resources and actions the integration requires in one or more Storyblok spaces.

> [!NOTE]
> Custom integrations is only available on specific plans. Learn more on the [pricing page](https://www.storyblok.com/pricing).

Custom integrations are private external applications that developers build for their Storyblok organizations. Unlike [plugins](/docs/plugins), these integrations run outside Storyblok and have no interface in the Visual Editor or the space.

## OAuth flow

The OAuth scoped grants flow follows the OAuth 2.0 [Authorization Code Grant](https://www.rfc-editor.org/info/rfc6749/#section-4.1) with [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636).

The following steps outline the process in order:

1.  **Request authorization**
    
    Your custom integration sends the user to Storyblok to grant access, listing the permissions and spaces it needs.
    
2.  **Grant access**
    
    On the consent page, the user reviews the requested permissions, adjusts them if needed, and chooses which spaces to authorize.
    
3.  **Receive the authorization code**
    
    Storyblok sends the user back to your custom integration with a temporary authorization code. If the user declines, Storyblok returns an error instead.
    
4.  **Exchange the code for a token**
    
    Your custom integration swaps the authorization code for an access token, and a refresh token if the user approved offline access.
    
5.  **Call the Management API**
    
    The custom integration uses the access token to make authorized requests to the Management API.
    
6.  **Refresh or revoke the token**
    
    The custom integration refreshes the access token when it expires, or revokes it to revoke access.
    

## Regions

The OAuth endpoints support all regions. Token exchange, refresh, revocation, and grant introspection all use `mapi.storyblok.com`, regardless of the region of the user’s spaces. The consent page lists the user’s eligible spaces across all regions.

Send Management API requests to the space’s region [base URL](/docs/api/management#base-urls). If a grant includes spaces from multiple regions, first call the [grant introspection](/docs/concepts/oauth-scoped-grants#grant-introspection) endpoint and get the region of each authorized space.

## Scopes

Permissions use the form `resource:action`, enforced per resource. Actions follow a hierarchy: the `publish` action includes everything that `write` allows, and `write` includes everything that `read` allows.

For example, a `stories:publish` token can write and read stories, while a `stories:read` token can only read stories.

A grant applies only to the scopes and the spaces the user selects on the consent page.

| Available actions | Resources |
| --- | --- |
| read, write, publish | `stories`, `releases` |
| read, write | `ai_branding`, `assets`, `asset_folders`, `collaborators`, `comments`, `components`, `datasources`, `datasource_entries`, `spaces`, `tags`, `users`, `webhooks`, `workflows` |
| write | `ai_translation` |
| read | `statistics` |
| special | `offline_access` |

## Authorization

Before sending the authorization request, generate the PKCE values:

| Value | Description |
| --- | --- |
| `code_verifier` | 43–128 characters. Store it securely for the authorization code exchange. |
| `code_challenge` | `BASE64URL(SHA256(code_verifier))` |
| `code_challenge_method` | `S256`, the only supported method |

Construct the authorization request URL with the following attributes:

| Attribute | Value |
| --- | --- |
| `client_id` | Client ID of the integration |
| `redirect_uri` | The configured redirection endpoint |
| `response_type` | `code` |
| `scope` | A space- or `+`\-separated list of `resource:action` scopes. Include `offline_access` to receive a refresh token. |
| `state` | Required. A random value that prevents CSRF attacks. |
| `code_challenge` | The PKCE code challenge |
| `code_challenge_method` | `S256` |

For example, the URL would look like:

```bash
https://app.storyblok.com/#/oauth/init?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&scope=stories:read stories:write offline_access&state=SOME_UUID&code_challenge=SHA_CODE&code_challenge_method=S256
```

### User consent page

After you redirect the user to the authorization endpoint, Storyblok shows the consent page, where users review and grant access to your custom integration.

The consent page groups the requested permissions by resource, such as **Asset folders** or **Stories**. Each resource lists one or more permission levels—`read`, `write`, or `publish`—that the user can control.

It also lets users select spaces where the grant applies.

Users must select at least one resource permission and one space.

> [!NOTE]
> The permissions your custom integration receives may differ from what it requested, since users can grant fewer permissions to any number of spaces. To confirm the granted access, check the `scope` value in the token response, or call the [grant introspection](/docs/concepts/oauth-scoped-grants#grant-introspection) endpoint.

### Authorization response

When the user approves the permissions, Storyblok redirects to your `redirect_uri` with query parameters:

```bash
{redirect_uri}?code={code}&state={state}
```

| Parameter | Description |
| --- | --- |
| `redirect_uri` | The redirection endpoint configured in your custom integration settings |
| `code` | A unique code that Storyblok generates for your integration to request access and refresh tokens |
| `state` | The exact value from the authorization request. Use it to verify the request and prevent CSRF attacks. |

When the user denies the permissions, Storyblok redirects to your `redirect_uri` with an `error` parameter and the `state` value:

```bash
{redirect_uri}?error=access_denied&state={state}
```

The `error` value is `access_denied`. Verify the `state` before you proceed.

## Access token management

Request, introspect, refresh, and revoke access tokens with the following operations:

### Access token request

After you verify `state` and extract the authorization code, send a `POST` request to the token endpoint. Storyblok exchanges the code for an access token. The request has the following attributes:

| Attribute | Value |
| --- | --- |
| `grant_type` | `authorization_code` |
| `code` | The authorization code from the redirect |
| `code_verifier` | The PKCE verifier that matches your `code_challenge` |
| `client_id` | The custom integration’s client ID |
| `client_secret` | The custom integration’s client secret |
| `redirect_uri` | The same redirection endpoint as in the authorization request |

```bash
POST https://mapi.storyblok.com/oauth/token
```

A successful response returns the following:

```json
{
  "access_token": "<ACCESS_TOKEN>",
  "refresh_token": "<REFRESH_TOKEN>",
  "token_type": "bearer",
  "expires_in": 900,
  "scope": "stories:read offline_access"
}
```

| Token | Prefix | Lifetime |
| --- | --- | --- |
| Authorization code | —   | 60 seconds, single-use (concurrent double-exchange revokes the grant) |
| Access token | `sb_oat_` | Valid for 15 minutes |
| Refresh token | `sb_ort_` | Valid for one month. Requires `offline_access` scope. |

The response includes a refresh token only if the user approves the `offline_access` scope. Otherwise, it includes an access token.

> [!NOTE]
> The response shows each token value only _once_.

### Grant introspection

To retrieve the grant’s scopes, expiry, the custom integration’s identity, and the spaces along and regions, call the grant introspection endpoint with the grant’s access token.

```bash
GET https://mapi.storyblok.com/v1/oauth/grant
Authorization: Bearer <ACCESS_TOKEN>
```

A successful response returns the following:

```json
{
  "grant": {
    "scopes": ["stories:read", "stories:write"],
    "expires_at": "2026-07-06T12:00:00Z",
    "app": { "name": "<APP_NAME>", "client_id": "<CLIENT_ID>" },
    "spaces": [
      { "id": 123, "region": "eu" },
      { "id": 100000123, "region": "us" }
    ]
  }
}
```

An expired or revoked token returns `401 Unauthorized` error.

### Access token refresh

To refresh an access token, send a `POST` request with the following attributes:

| Attribute | Value |
| --- | --- |
| `grant_type` | `refresh_token` |
| `refresh_token` | The current refresh token |
| `client_id` | The custom integration’s client ID |
| `client_secret` | The custom integration’s client secret |

```bash
POST https://mapi.storyblok.com/oauth/token
```

A refresh token works once. When you use it, the API returns a new access token along with a new refresh token, and the one you sent is no longer valid—store the replacement. Each refresh also restarts a one-month countdown, so a custom integration that’s inactive for a month must reauthorize.

### Access token revocation

To revoke a token in a grant, send a `POST` request with the following attributes:

| Attribute | Required | Description |
| --- | --- | --- |
| `token` | Yes | The raw access token or refresh token to revoke. |
| `client_id` | Yes | The integration’s client ID |
| `client_secret` | Yes | The integration’s client secret |
| `token_type_hint` | No  | The token type: `access_token` or `refresh_token` |

```bash
POST https://mapi.storyblok.com/oauth/revoke
```

When you revoke either the access token or the refresh token, both become invalid. To obtain access again, the user must reauthorize the integration.

## Authenticated requests to the Management API

When sending API requests, include the access token in the `Authorization` header:

```bash
Authorization: Bearer <ACCESS_TOKEN>
```

## Pagination

-   [Previous: Internationalization](/docs/concepts/internationalization)
-   [Next: References](/docs/concepts/references)
