Skip to content

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.

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 with Proof Key for Code Exchange (PKCE), short-lived access tokens, rotating refresh tokens, and token revocation to improve security.

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.

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 for the intended space region.

If a grant includes spaces from multiple regions, use the grant introspection endpoint to determine the region of each authorized space before sending Management API requests.

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

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

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:

Terminal window
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

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.

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:

Terminal window
{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:

Terminal window
{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.

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
Terminal window
POST https://mapi.storyblok.com/oauth/token

A successful response returns the following:

{
"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.

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

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.

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

A successful response returns the following:

{
"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.

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
Terminal window
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.

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.
Terminal window
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

Section titled “Authenticated requests to the Management API”

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

Terminal window
Authorization: Bearer <ACCESS_TOKEN>

For additional details on Management API endpoints, refer to the Storyblok Management API documentation.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.