---
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 Scoped Grants.
url: https://www.storyblok.com/docs/plugins/oauth-scoped-grants
---

# OAuth 2.0 Scoped Grants

Custom integrations are external applications created by developers that operate independently of the Storyblok editor and spaces. They do not render within Storyblok. Instead, they authorize using OAuth 2.0 Scoped Grants and then connect to Storyblok through the Management API.

Custom integrations are different from plugins. Field, tool, and space plugins run inside Storyblok, in the Visual Editor, or in a dedicated section of a space to enhance the editing experience. Custom integrations have no interface inside Storyblok, they run as external applications. For details on plugins, see the [plugins documentation](/docs/plugins).

> [!NOTE]
> Custom integrations are a premium feature. Learn more on the [pricing](https://www.storyblok.com/pricing) page.

OAuth 2.0 Scoped Grants is an authorization flow that allows your custom integration app to securely obtain a Management API access token with fine-grained permissions for specific resources in one or more Storyblok spaces.

Unlike the standard OAuth 2.0 flow, which grants read or write access to a single space, OAuth 2.0 Scoped Grants lets developers authorize only the resources and actions your custom integration requires across multiple spaces with a single authorization. It also uses [Authorization Code Grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636), short-lived access tokens, rotating refresh tokens, and token revocation to improve security.

> [!NOTE]
> Custom integrations are private to the organization that created them and are available only to the organization’s owners and admins.

## OAuth Scoped Grants flow

The OAuth Scoped Grants flow follows the OAuth 2.0 Authorization Code Grant with PKCE. The following steps outline the process in order:

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

## Regions

The entire OAuth Scoped Grants flow is not specific to a region. You can use a single OAuth host for authorization, token exchange, token refresh, token revocation, and grant introspection, regardless of where the user’s spaces are located.

During the authorization process, the user consent page shows the user’s eligible spaces across all supported regions.

However, the region becomes important when using the access token to call the Management API. Send API requests to the [Management API host](/docs/api/management) for the intended space region.

If a grant includes spaces from multiple regions, use the [grant introspection](/docs/plugins/oauth-scoped-grants#grant-introspection) endpoint to determine the region of each authorized space before sending Management API requests.

## Scopes

Permissions take the form `resource:action`, enforced per resource. Actions follow a hierarchy. Each higher level includes all actions from the levels below.

```text
publish  ⊃  write  ⊃  read
```

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

A grant is further limited to the scopes and spaces the user selects at the consent page.

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

## Authorization

Before sending the authorization request, generate a PKCE pair:

-   `code_verifier`: 43–128 characters.
-   `code_challenge`: `BASE64URL(SHA256(code_verifier))`.

Set `code_challenge_method` to `S256` (the only supported method). Store the `code_verifier` securely. You’ll need it when exchanging the authorization code for an access token.

Construct the authorization request URL using the following attributes:

-   `client_id`: Client ID of the integration
-   `redirect_uri`: one of your configured redirection endpoints
-   `response_type`: `code`
-   `scope`: A space- or `+`\-separated list of `resource:action` scopes. Include `offline_access` to receive a refresh token.
-   `state`: Randomly generated value to prevent CSRF attacks. It is required.
-   `code_challenge` and `code_challenge_method`: PKCE, using `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 they review and approve the access your custom integration is requesting.

The consent page shows the name of your custom integration and groups the requested permissions by resource, such as **Asset folders** or **Stories**. Each resource lists one or more permission levels (read, write, publish) as checkboxes, pre-checked by default. The user can uncheck any of them.

Spaces are selected in a separate **Select spaces** field. No space is selected by default, so the user must choose which spaces the custom integration can access. The grant applies only to the spaces the user selects in the consent page.

At least one resource permission and one space must be selected.

> [!NOTE]
> The permissions your custom integration receives may differ from what it requested, since the user can uncheck permissions or grant fewer spaces. Always confirm the granted access by inspecting the `scope` value in the token response, or by calling the [grant introspection](/docs/plugins/oauth-scoped-grants#grant-introspection) endpoint.

### Authorization response

After the user approves your custom integration’s permissions, Storyblok redirects to the specified URL with additional query parameters. The redirected URL has the following structure:

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

The parameters provided in the URL are:

-   `redirect_uri`: The URL configured in your custom integration settings as the redirection endpoint.
-   `code`: A unique code generated by Storyblok for your integration to request access and refresh tokens.
-   `state`: The exact value sent in the initial authorization request. This value is used to verify the request and prevent CSRF attacks.

If the user denies your custom integration’s permissions, Storyblok redirects to your `redirect_uri` with an `error` parameter along with the `state` value:

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

`error` is `access_denied` when the user declines. Verify `state` as you would on the success path before acting on the response.

## Access token management

### Access token request

Once you have validated the `state` and extracted the authorization code, exchange it for an access token by sending a `POST` request with these attributes:

-   `grant_type`: `authorization_code`
-   `code`: the authorization code from the redirect
-   `code_verifier`: the PKCE verifier matching your `code_challenge`
-   `client_id`: the custom integration’s client id
-   `client_secret`: the custom integration’s client secret
-   `redirect_uri`

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

> [!NOTE]
> The refresh token is only available if the user approved the `offline_access` scope, otherwise, only an access token is provided.

A token value is shown **once** in the response and cannot be retrieved again.

### Grant introspection

Call the grant introspection endpoint with the grant’s own access token to retrieve its scopes, expiry, the custom integration’s identity, and the granted spaces along with their regions.

```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" }
    ]
  }
}
```

Use each space’s `region` to choose the correct Management API host when sending API requests for that space. An expired or revoked token returns `401 Unauthorized` error.

### Access token refresh

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

-   `grant_type`: `refresh_token`
-   `refresh_token`: your current refresh token
-   `client_id` and `client_secret`

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

Refresh tokens rotate. Each successful refresh returns a new access token and immediately invalidates the previous refresh token. Always store and use the latest refresh token. The one-month window for the refresh token resets on each refresh. An integration that is not active for a full month must re-authorize again.

### Access token revocation

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

-   `token`: the raw token you want to revoke (either an access token or a refresh token)
-   `client_id` and `client_secret`
-   optional `token_type_hint`: either `access_token` or `refresh_token`. Hint only.

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

If either the access token or the refresh token is revoked, both tokens will no longer be valid. To obtain access again, the user must re-authorize.

## Authenticated requests to the Management API

Include the access token in the `Authorization` header when sending API requests:

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

For additional details on Management API endpoints, refer to the Storyblok [Management API documentation](https://www.storyblok.com/docs/api/management).

## Related resources

[OAuth 2.0 Authorization Flow](/docs/plugins/oauth-authorization-flow)

[Management API documentation](/docs/api/management)

[Proof Key for Code Exchange (RFC 7636)](https://tools.ietf.org/html/rfc7636)

## Pagination

-   [Previous: OAuth 2.0 Authorization Flow](/docs/plugins/oauth-authorization-flow)
-   [Next: Introduction](/docs/plugins/field-plugins-legacy)
