diff --git a/blog/2026-03-20-storyblok-adapters.mdx b/blog/2026-03-20-storyblok-adapters.mdx
new file mode 100644
index 0000000..225a354
--- /dev/null
+++ b/blog/2026-03-20-storyblok-adapters.mdx
@@ -0,0 +1,37 @@
+---
+title: Storyblok Adapter Setup and Usage
+authors: [jannik]
+tags: [storyblok, symfony]
+---
+
+The Storyblok docs now include a complete guide for the adapter-based setup.
+
+## New Documentation
+
+- [Adapters guide][1] with setup, configuration and usage.
+- Dedicated docs for [Content API][5], [Management API][6], [ID Slug Mapper][7], and [Request Validator][8].
+- New docs for [Backend Edit URL Helper][9] and [Definitions Synced Event][10].
+- New [Asset Proxy][12] section with setup/configuration and usage details.
+- New [More Features][13] section for miscellaneous mostly-internal helper features.
+
+## Updated Existing Pages
+
+- [Storyblok installation page][2] now reflects the adapter-based flow.
+- [CLI docs][3] now describe adapter-scoped commands and include the post-sync `StoryblokDefinitionsSyncedEvent` hook.
+- [Webhook docs][4] now use `AppStoryblokAdapter` examples.
+- [Storyblok index][2] now links to the new Asset Proxy section and includes an events overview.
+- `More Features` was moved to the bottom of the Storyblok sidebar order.
+
+
+[1]: /docs/php/symfony/storyblok/adapters
+[2]: /docs/php/symfony/storyblok
+[3]: /docs/php/symfony/storyblok/cli
+[4]: /docs/php/symfony/storyblok/webhook
+[5]: /docs/php/symfony/storyblok/api/content-api
+[6]: /docs/php/symfony/storyblok/api/management-api
+[7]: /docs/php/symfony/storyblok/more-features/id-slug-mapper
+[8]: /docs/php/symfony/storyblok/more-features/request-validator
+[9]: /docs/php/symfony/storyblok/more-features/backend-edit-url-helper
+[10]: /docs/php/symfony/storyblok/definitions-synced-event
+[12]: /docs/php/symfony/storyblok/asset-proxy
+[13]: /docs/php/symfony/storyblok/more-features
diff --git a/docs/php/symfony/storyblok/adapters.mdx b/docs/php/symfony/storyblok/adapters.mdx
new file mode 100644
index 0000000..3d01e94
--- /dev/null
+++ b/docs/php/symfony/storyblok/adapters.mdx
@@ -0,0 +1,171 @@
+---
+sidebar_position: -1
+---
+
+# Adapters
+
+The Storyblok bundle uses adapters to connect to one or multiple Storyblok spaces.
+Each adapter is meant to connect to a single Storyblok space, configured via `StoryblokConfig`.
+
+
+## Setup
+
+Create one adapter class per Storyblok space:
+
+```php title="src/Storyblok/Adapter/AppStoryblokAdapter.php"
+contentApi
+```
+
+### Management API
+
+[Management API docs](./api/management-api)
+
+Manage your Storyblok space via the Management API, for example when syncing component definitions.
+
+```php
+$appStoryblok->managementApi
+```
+
+### ID Slug Mapper
+
+[ID Slug Mapper docs](./more-features/id-slug-mapper)
+
+Resolve between Story IDs and slugs using the content API.
+
+```php
+$appStoryblok->idSlugMapper
+```
+
+### Request Validator
+
+[Request Validator docs](./more-features/request-validator)
+
+Validate incoming webhook requests against your adapter webhook configuration.
+
+```php
+$appStoryblok->requestValidator
+```
+
+
+
+## Usage
+
+If you know which Storyblok adapter you want to use, you can inject it directly:
+
+```php
+function example (AppStoryblokAdapter $storyblok)
+{
+ $storyblok->...
+}
+```
+
+Sometimes you need to fetch the storyblok depending on a dynamic value, so you need to use the `StoryblokAdapterRegistry` for that:
+
+```php
+function example (StoryblokAdapterRegistry $registry)
+{
+ $registry->getByKey("app");
+ $registry->getBySpaceId("123456");
+}
+```
+
+[snail]: /docs/php/symfony/snail/
diff --git a/docs/php/symfony/storyblok/api/content-api.mdx b/docs/php/symfony/storyblok/api/content-api.mdx
new file mode 100644
index 0000000..dee3a21
--- /dev/null
+++ b/docs/php/symfony/storyblok/api/content-api.mdx
@@ -0,0 +1,83 @@
+# Content API
+
+The Content API is available on the adapter as:
+
+```php
+$appStoryblok->contentApi
+```
+
+Use it to fetch and hydrate Storyblok content for the configured adapter space.
+
+## Main Methods
+
+### `fetchSingleStory()`
+
+Fetches a single story by slug, id or uuid.
+
+```php
+$appStoryblok->contentApi->fetchSingleStory("home");
+```
+
+### `fetchStories()`
+
+Fetches stories for a specific story class.
+
+```php
+$appStoryblok->contentApi->fetchStories(PageStory::class, "pages/*");
+```
+
+### `fetchAllStories()`
+
+Fetches all stories (optionally filtered by slug pattern).
+
+```php
+$appStoryblok->contentApi->fetchAllStories("pages/*");
+```
+
+### `getSpaceInfo()`
+
+Returns metadata for the current Storyblok space.
+
+```php
+$appStoryblok->contentApi->getSpaceInfo();
+```
+
+### `fetchDatasourceEntries()`
+
+Fetches datasource entries from the Content API.
+
+```php
+$appStoryblok->contentApi->fetchDatasourceEntries("countries");
+```
+
+### `fetchFoldersInPath()`
+
+Fetches folders below a slug prefix.
+
+```php
+$appStoryblok->contentApi->fetchFoldersInPath("pages");
+```
+
+### `fetchFolderTitleMap()`
+
+Fetches a map of local folder URL to folder title.
+
+```php
+$appStoryblok->contentApi->fetchFolderTitleMap("pages");
+```
+
+### `fetchAllLinks()`
+
+Fetches all Storyblok links.
+
+```php
+$appStoryblok->contentApi->fetchAllLinks();
+```
+
+### `fetchSignedAssetUrl()`
+
+Fetches signed asset data for private assets.
+
+```php
+$appStoryblok->contentApi->fetchSignedAssetUrl("https://a.storyblok.com/f/...");
+```
diff --git a/docs/php/symfony/storyblok/api/index.mdx b/docs/php/symfony/storyblok/api/index.mdx
new file mode 100644
index 0000000..63a16ec
--- /dev/null
+++ b/docs/php/symfony/storyblok/api/index.mdx
@@ -0,0 +1,89 @@
+# API
+
+Besides the [content api] and [management api], it is recommended to add a storyblok API, which is a wrapper for the content api and helps with purpose-built fetch methods.
+
+This way all your knowledge about the structure and types of your Storyblok space are encapsulated in one place.
+
+
+```php
+use Symfony\Component\DependencyInjection\Attribute\Exclude;
+
+#[Exclude]
+class AppStoryblokApi
+{
+ public function __construct (
+ private readonly ContentApi $contentApi,
+ ) {}
+
+ /**
+ * @return NewsStory[]
+ */
+ public function fetchNews (string $pageTree, string $locale)
+ {
+ return $this->contentApi->fetchStories(
+ storyType: NewsStory::class,
+ slug: \sprintf("%s/%s/news/*", $pageTree, $locale),
+ version: $version,
+ );
+ }
+}
+```
+
+Wire it up in your adapter:
+
+```php
+class AppStoryblokAdapter
+{
+ public readonly AppStoryblokApi $api;
+
+ public function __construct (...)
+ {
+ $this->api = new AppStoryblokApi($this->contentApi);
+ }
+```
+
+And use it like this:
+
+```php
+$appStoryblok->api->fetchNews("website", "de-de")
+```
+
+## Caching
+
+If your API client is caching, you need to reset the cache regularly. As the API is not a service itself but just wrapped inside the adapter, you need to forward the call.
+
+First mark your adapter as resettable:
+
+```php
+use Symfony\Contracts\Service\ResetInterface;
+
+class AppStoryblokAdapter extends AbstractStoryblokAdapter implements ResetInterface
+{
+ public readonly AppStoryblokApi $api;
+
+ public function reset ()
+ {
+ $this->api->reset();
+ }
+}
+```
+
+Then implement the cache reset in your API client:
+
+```php
+use Symfony\Contracts\Service\ResetInterface;
+
+class AppStoryblokApi implements ResetInterface
+{
+ public function reset ()
+ {
+ $this->cache = [];
+ }
+}
+```
+
+
+
+
+[content api]: ./content-api
+[management api]: ./management-api
diff --git a/docs/php/symfony/storyblok/api/management-api.mdx b/docs/php/symfony/storyblok/api/management-api.mdx
new file mode 100644
index 0000000..71612af
--- /dev/null
+++ b/docs/php/symfony/storyblok/api/management-api.mdx
@@ -0,0 +1,136 @@
+# Management API
+
+The Management API is available on the adapter as:
+
+```php
+$appStoryblok->managementApi
+```
+
+Use it for write operations and administration tasks in the configured Storyblok space.
+
+## Main Methods
+
+### `syncComponent()`
+
+Creates or updates a component definition.
+
+```php
+$appStoryblok->managementApi->syncComponent($componentConfig);
+```
+
+### `getOrCreatedComponentGroupUuid()`
+
+Resolves or creates a component group uuid.
+
+```php
+$appStoryblok->managementApi->getOrCreatedComponentGroupUuid("Content");
+```
+
+### `fetchAllRegisteredComponents()`
+
+Returns all component keys currently registered in the space.
+
+```php
+$appStoryblok->managementApi->fetchAllRegisteredComponents();
+```
+
+### `fetchComponentDefinitions()`
+
+Fetches all component definitions.
+
+```php
+$appStoryblok->managementApi->fetchComponentDefinitions();
+```
+
+### `syncDatasourceEntries()`
+
+Syncs datasource entries from your local mapping.
+
+```php
+$appStoryblok->managementApi->syncDatasourceEntries("countries", $values);
+```
+
+### `fetchDatasourceEntries()`
+
+Fetches datasource entries via the Management API.
+
+```php
+$appStoryblok->managementApi->fetchDatasourceEntries("countries");
+```
+
+### `fetchAllAssets()`
+
+Fetches metadata of all assets.
+
+```php
+$appStoryblok->managementApi->fetchAllAssets();
+```
+
+### `updateAsset()`
+
+Updates an asset by id.
+
+```php
+$appStoryblok->managementApi->updateAsset($assetId, $payload);
+```
+
+### `fetchAssetFolders()`
+
+Fetches the full asset folder tree.
+
+```php
+$appStoryblok->managementApi->fetchAssetFolders();
+```
+
+### `fetchAssetData()`
+
+Fetches metadata for a specific asset id.
+
+```php
+$appStoryblok->managementApi->fetchAssetData($assetId);
+```
+
+### `exportTranslationsXmlFile()`
+
+Exports the translation XML for a story.
+
+```php
+$appStoryblok->managementApi->exportTranslationsXmlFile($storyId);
+```
+
+### `importTranslationsXmlFile()`
+
+Imports translation XML for a story.
+
+```php
+$appStoryblok->managementApi->importTranslationsXmlFile($storyId, $xmlContent);
+```
+
+### `fetchStory()`
+
+Fetches a raw management API story payload by story id.
+
+```php
+$appStoryblok->managementApi->fetchStory($storyId);
+```
+
+### `updateStory()`
+
+Updates a story by story id.
+
+```php
+$appStoryblok->managementApi->updateStory($storyId, $storyJson);
+```
+
+### `sendRequest()`
+
+Low-level method to send arbitrary requests to the Storyblok Management API.
+
+```php
+$appStoryblok->managementApi->sendRequest("components");
+```
+
+## Deprecated
+
+`fetchFolderTitleMap()` and `fetchFoldersInPath()` are deprecated in `ManagementApi`.
+Use the Content API equivalents instead.
diff --git a/docs/php/symfony/storyblok/asset-proxy/index.mdx b/docs/php/symfony/storyblok/asset-proxy/index.mdx
new file mode 100644
index 0000000..f820495
--- /dev/null
+++ b/docs/php/symfony/storyblok/asset-proxy/index.mdx
@@ -0,0 +1,17 @@
+# Asset Proxy
+
+The Storyblok Asset Proxy lets your application serve Storyblok assets through your own app URL.
+
+Instead of directly serving `https://a.storyblok.com/...`, the URL is rewritten to your app, signed, and cached on disk.
+
+## What This Means for Your Application
+
+- Asset URLs are served from your application domain.
+- Asset files are cached locally in `var/storyblok/assets` by default.
+- Replaced and deleted assets are automatically invalidated when asset webhooks are enabled.
+- Proxy URLs are signed, so only valid generated URLs are accepted.
+
+## Pages
+
+- [Setup and Configuration](./setup-and-configuration)
+- [Usage](./usage)
diff --git a/docs/php/symfony/storyblok/asset-proxy/setup-and-configuration.mdx b/docs/php/symfony/storyblok/asset-proxy/setup-and-configuration.mdx
new file mode 100644
index 0000000..9cb4c49
--- /dev/null
+++ b/docs/php/symfony/storyblok/asset-proxy/setup-and-configuration.mdx
@@ -0,0 +1,55 @@
+---
+sidebar_position: 10
+---
+
+# Setup and Configuration
+
+## Routing
+
+Load the bundle routes so the asset proxy endpoint is available:
+
+```yaml title="config/routes/storyblok.yaml"
+storyblok:
+ prefix: /storyblok/
+ resource: "@TorrStoryblokBundle/config/routes.yaml"
+```
+
+The proxy route is:
+
+```txt
+/storyblok/asset/{spaceId}/{path}
+```
+
+## Storage Path
+
+By default, proxied files are stored in:
+
+```txt
+%kernel.project_dir%/var/storyblok/assets
+```
+
+You can override this path via service configuration:
+
+```yaml title="config/services.yaml"
+services:
+ Torr\Storyblok\Assets\Proxy\AssetProxy:
+ arguments:
+ $storagePath: "%kernel.project_dir%/var/custom-storyblok-assets"
+```
+
+## Webhook Integration for Cache Invalidation
+
+Asset proxy cache invalidation is triggered by asset webhooks:
+
+- `AssetReplaced`
+- `AssetDeleted`
+
+So make sure your Storyblok webhook is configured and asset events are enabled.
+
+## Clearing the Proxy Cache
+
+Clear all cached proxy files:
+
+```bash
+bin/console storyblok:assets:clear-proxy-storage
+```
diff --git a/docs/php/symfony/storyblok/asset-proxy/usage.mdx b/docs/php/symfony/storyblok/asset-proxy/usage.mdx
new file mode 100644
index 0000000..30165d5
--- /dev/null
+++ b/docs/php/symfony/storyblok/asset-proxy/usage.mdx
@@ -0,0 +1,42 @@
+---
+sidebar_position: 20
+---
+
+# Usage
+
+## Rewriting Asset URLs
+
+Use `AssetProxyUrlGenerator` to rewrite Storyblok asset URLs to signed proxy URLs:
+
+```php
+$assetProxyUrlGenerator->rewriteAssetUrl($storyblokAssetUrl);
+```
+
+If the URL is not a Storyblok asset URL, it is returned unchanged.
+
+## Request Flow
+
+1. Your app generates a signed proxy URL.
+2. The request hits `/storyblok/asset/{spaceId}/{path}`.
+3. The signature is validated.
+4. If the file is not cached, it is downloaded from Storyblok and stored locally.
+5. The cached file is streamed to the response.
+
+If the URL signature is invalid, the proxy returns `404`.
+
+## Download Behavior
+
+You can force file download by adding `download` as query parameter:
+
+```txt
+?download=1
+```
+
+Without it, files are returned as inline responses.
+
+## Application Impact
+
+- First request for an asset path is slower (cache miss + download).
+- Following requests are faster (local file hit).
+- Disk usage grows with cached assets.
+- Webhook invalidation keeps replaced/deleted assets fresh.
diff --git a/docs/php/symfony/storyblok/cli.mdx b/docs/php/symfony/storyblok/cli.mdx
index af3c677..db0054e 100644
--- a/docs/php/symfony/storyblok/cli.mdx
+++ b/docs/php/symfony/storyblok/cli.mdx
@@ -1,7 +1,3 @@
----
-sidebar_position: 20
----
-
# CLI
To integrate your local definitions with Storyblok, you need to push your config to their API.
@@ -13,11 +9,26 @@ Call the command via:
bin/console storyblok:*
```
-## `storyblok:components:overview`
+## Adapter Scope
+
+All commands accept optional adapter keys.
+If no key is passed, all adapters are used.
+
+```bash
+bin/console storyblok:debug
+bin/console storyblok:debug main
+bin/console storyblok:debug main marketing
+```
+
+
+## `storyblok:debug`
-Renders an overview of all locally registered components, their names and components + stories.
+Renders debug details for your configured adapter(s):
-It also renders a list of all components configured in Storyblok that don't match a local definition.
+- Adapter key and Storyblok space metadata
+- Components used in each adapter
+- Unknown components in the space
+- Component library and asset proxy stats
:::tip
If you have manually managed components in Storyblok, you can ignore them in this overview.
@@ -26,10 +37,22 @@ However, renamed components are not automatically removed in Storyblok, so this
:::
+## `storyblok:definitions:validate`
+
+Validates local component definitions for the selected adapter(s).
+
+
## `storyblok:definitions:sync`
-Pushes your local component definitions to Storyblok.
+Pushes local component definitions to Storyblok for the selected adapter(s).
By default, you are asked to confirm to sync the structure.
You can pass `--force` to automatically confirm the command. This can be used to automate your deployment.
+
+:::caution
+`--force` is only allowed in production.
+:::
+
+On successful sync, the bundle dispatches `StoryblokDefinitionsSyncedEvent`.
+See [Definitions Synced Event](./definitions-synced-event) for listener setup.
diff --git a/docs/php/symfony/storyblok/components/index.mdx b/docs/php/symfony/storyblok/components/index.mdx
index 7d5b504..50e29b5 100644
--- a/docs/php/symfony/storyblok/components/index.mdx
+++ b/docs/php/symfony/storyblok/components/index.mdx
@@ -1,7 +1,3 @@
----
-sidebar_position: 10
----
-
# Components
Storyblok works and looks like a virtual filesystem. You can have folders and documents inside of them.
diff --git a/docs/php/symfony/storyblok/components/recommended-directory-structure.mdx b/docs/php/symfony/storyblok/components/recommended-directory-structure.mdx
index ca97290..df18346 100644
--- a/docs/php/symfony/storyblok/components/recommended-directory-structure.mdx
+++ b/docs/php/symfony/storyblok/components/recommended-directory-structure.mdx
@@ -7,6 +7,7 @@ sidebar_position: 30
```text
src/
└─ Storyblok/
+ ├─ Adapter/
├─ Api/
├─ Component/
│ ├─ Block/
diff --git a/docs/php/symfony/storyblok/definitions-synced-event.mdx b/docs/php/symfony/storyblok/definitions-synced-event.mdx
new file mode 100644
index 0000000..70fa956
--- /dev/null
+++ b/docs/php/symfony/storyblok/definitions-synced-event.mdx
@@ -0,0 +1,29 @@
+# Definitions Synced Event
+
+After a successful `storyblok:definitions:sync`, the bundle dispatches `StoryblokDefinitionsSyncedEvent`.
+
+## Event Data
+
+The event provides:
+
+- `io`: `TorrStyle` instance used by the sync command.
+- `adapter`: the adapter that was synced.
+
+## Listener Example
+
+```php
+use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
+use Torr\Storyblok\Event\StoryblokDefinitionsSyncedEvent;
+
+#[AsEventListener]
+public function onDefinitionsSynced (StoryblokDefinitionsSyncedEvent $event) : void
+{
+ $event->io->writeln("Run post-sync work...");
+ $event->adapter->managementApi;
+}
+```
+
+## Notes
+
+- If multiple adapters are synced in one command run, one event is dispatched per adapter.
+- The event is only dispatched on successful sync.
diff --git a/docs/php/symfony/storyblok/image-focus.mdx b/docs/php/symfony/storyblok/image-focus.mdx
index b890dc3..9ddf3a5 100644
--- a/docs/php/symfony/storyblok/image-focus.mdx
+++ b/docs/php/symfony/storyblok/image-focus.mdx
@@ -1,7 +1,3 @@
----
-sidebar_position: 100
----
-
# Image Focus
In Storyblok, you can set a focus point in a image.
diff --git a/docs/php/symfony/storyblok/index.mdx b/docs/php/symfony/storyblok/index.mdx
index ff8922b..3220ab2 100644
--- a/docs/php/symfony/storyblok/index.mdx
+++ b/docs/php/symfony/storyblok/index.mdx
@@ -18,31 +18,6 @@ This bundle provides helpers for
## Installation
-You need to define various environment variables:
-
-```dotenv
-# your space id as number
-STORYBLOK_SPACE_ID=
-
-# your management token
-STORYBLOK_MANAGEMENT_TOKEN=
-
-# your content token – with `preview` scope
-STORYBLOK_CONTENT_TOKEN=
-```
-
-Then add the base config in `config/packages/storyblok.yaml`:
-
-```yaml
-storyblok:
- space_id: "%env(STORYBLOK_SPACE_ID)%"
- management_token: "%env(STORYBLOK_MANAGEMENT_TOKEN)%"
- content_token: "%env(STORYBLOK_CONTENT_TOKEN)%"
-```
-
-
-Then install the package:
-
```shell
composer require 21torr/storyblok
```
@@ -52,38 +27,30 @@ If you are using Symfony Flex you are all set.
Manual configuration
- You need to define various environment variables:
+ Load the routing:
+
+ ```yaml title="config/routes/storyblok.yaml"
+ storyblok:
+ prefix: /storyblok/
+ resource: '@TorrStoryblokBundle/config/routes.yaml'
+ ```
+
- ```dotenv title=".env"
- # your space id as number
- STORYBLOK_SPACE_ID=
+## Adapter Setup
- # your management token
- STORYBLOK_MANAGEMENT_TOKEN=
+The bundle is adapter-based.
+You create one adapter per Storyblok space and configure each adapter with its own `StoryblokConfig`.
- # your content token – with `preview` scope
- STORYBLOK_CONTENT_TOKEN=
+Continue with the [adapter guide](./adapters) for setup, usage and configuration details.
- # the secret to validate webhooks
- STORYBLOK_WEBHOOK_SECRET=
- ```
+## Additional Helpers
- Then add the base config:
+- [Asset Proxy](./asset-proxy)
+- [More Features](./more-features)
- ```yaml title="config/packages/storyblok.yaml"
- storyblok:
- space_id: "%env(int:STORYBLOK_SPACE_ID)%"
- management_token: "%env(STORYBLOK_MANAGEMENT_TOKEN)%"
- content_token: "%env(STORYBLOK_CONTENT_TOKEN)%"
- webhook:
- secret: "%env(STORYBLOK_WEBHOOK_SECRET)%"
- ```
+## Events Overview
- and load the routing:
+The Storyblok bundle dispatches these events:
- ```yaml title="config/routes/storyblok.yaml"
- storyblok:
- prefix: /storyblok/
- resource: '@TorrStoryblokBundle/config/routes.yaml'
- ```
-
+- `StoryblokWebhookEvent` for incoming Storyblok webhooks ([Webhook docs](./webhook)).
+- `StoryblokDefinitionsSyncedEvent` after successful component sync ([Definitions Synced Event](./definitions-synced-event)).
diff --git a/docs/php/symfony/storyblok/more-features/backend-edit-url-helper.mdx b/docs/php/symfony/storyblok/more-features/backend-edit-url-helper.mdx
new file mode 100644
index 0000000..1bd709e
--- /dev/null
+++ b/docs/php/symfony/storyblok/more-features/backend-edit-url-helper.mdx
@@ -0,0 +1,23 @@
+# Backend Edit URL Helper
+
+`StoryblokBackendUrlGenerator` generates Storyblok backend edit links.
+
+## Usage
+
+Inject the helper:
+
+```php
+StoryblokBackendUrlGenerator $backendUrlGenerator
+```
+
+Generate edit URL by story object:
+
+```php
+$backendUrlGenerator->generateStoryEditUrl($story);
+```
+
+Generate edit URL by story id and space id:
+
+```php
+$backendUrlGenerator->generateStoryEditUrlById(123456, "98765");
+```
diff --git a/docs/php/symfony/storyblok/more-features/id-slug-mapper.mdx b/docs/php/symfony/storyblok/more-features/id-slug-mapper.mdx
new file mode 100644
index 0000000..6924eec
--- /dev/null
+++ b/docs/php/symfony/storyblok/more-features/id-slug-mapper.mdx
@@ -0,0 +1,24 @@
+# ID Slug Mapper
+
+The ID Slug Mapper is available on the adapter as:
+
+```php
+$appStoryblok->idSlugMapper
+```
+
+Use it to resolve a full slug from a Storyblok id or uuid.
+
+## Main Method
+
+### `getFullSlugById()`
+
+Returns the full slug for the given story id or uuid.
+If the id is unknown, it returns `null`.
+
+```php
+$appStoryblok->idSlugMapper->getFullSlugById("123456789");
+```
+
+```php
+$appStoryblok->idSlugMapper->getFullSlugById("95c8d1da-0f48-41f9-a376-c7bb8f72f23f");
+```
diff --git a/docs/php/symfony/storyblok/more-features/index.mdx b/docs/php/symfony/storyblok/more-features/index.mdx
new file mode 100644
index 0000000..8cddde9
--- /dev/null
+++ b/docs/php/symfony/storyblok/more-features/index.mdx
@@ -0,0 +1,13 @@
+---
+sidebar_position: 1000
+---
+
+# More Features
+
+These are miscellaneous Storyblok bundle features that are mainly used internally.
+
+In special cases, they can be used to implement custom requirements in your application.
+
+- [Backend Edit URL Helper](./backend-edit-url-helper)
+- [Request Validator](./request-validator)
+- [ID Slug Mapper](./id-slug-mapper)
diff --git a/docs/php/symfony/storyblok/more-features/request-validator.mdx b/docs/php/symfony/storyblok/more-features/request-validator.mdx
new file mode 100644
index 0000000..b69ceaa
--- /dev/null
+++ b/docs/php/symfony/storyblok/more-features/request-validator.mdx
@@ -0,0 +1,24 @@
+# Request Validator
+
+The request validator is available on the adapter as:
+
+```php
+$appStoryblok->requestValidator
+```
+
+Use it to validate incoming Storyblok webhook requests for the configured adapter.
+
+## Main Method
+
+### `isValidRequest()`
+
+Validates the request based on your adapter webhook config:
+
+- webhook signature (`webhook-signature`) for paid plans
+- optional URL secret fallback for unpaid plans (`allowUrlWebhookSecret`)
+
+```php
+$appStoryblok->requestValidator->isValidRequest($request, $urlSecret);
+```
+
+If both signature and URL secret are present at the same time, validation fails.
diff --git a/docs/php/symfony/storyblok/webhook.mdx b/docs/php/symfony/storyblok/webhook.mdx
index 9fe73ae..f313fc9 100644
--- a/docs/php/symfony/storyblok/webhook.mdx
+++ b/docs/php/symfony/storyblok/webhook.mdx
@@ -4,10 +4,10 @@ The Storyblok bundle automatically integrates in to all webhooks that are sent f
## Installation
-After setting up the bundle routes[^1], you need to configure the webhook URL in Storyblok:
+After setting up the bundle routes[^1] and your Storyblok adapter[^2], configure the webhook URL in Storyblok:
```txt
-https://.../storyblok/webhook
+https://.../storyblok/webhook/
```
@@ -19,10 +19,32 @@ You should always use webhook secrets to secure your webhook endpoints. The webh
On paid plans, you can use the native **Webhooks Secret** feature. Configure the webhook secret in Storyblok and in your bundle:
-```yaml title="config/packages/storyblok.yaml"
-storyblok:
- webhook:
- secret: "%env(STORYBLOK_WEBHOOK_SECRET)%"
+```php
+/my-secret
```
:::danger
@@ -127,3 +149,4 @@ Make sure to include useful information in the response, as you can then debug y
[^1]: Follow the [installation guide](./#installation). You can see the details about setting up the routing if you open the "manual installation" section.
+[^2]: See [adapter setup](./adapters#setup).