verstka / sdk
PHP SDK for Verstka API v2: open editor sessions, verify callbacks, download and process material content (media + fonts).
Requires
- php: >=8.2
- ext-hash: *
- ext-json: *
- ext-zip: *
- guzzlehttp/guzzle: ^7.8
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- laravel/framework: ^10.48|^11.0
- orchestra/testbench: ^8.28|^9.0
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^10.5
- symfony/framework-bundle: ^6.4|^7.0
- symfony/phpunit-bridge: ^6.4|^7.0
Suggests
- laravel/framework: For Laravel integration
- symfony/framework-bundle: For Symfony integration
Provides
None
Conflicts
None
Replaces
None
README
PHP SDK for Verstka API v2. Open the editor, handles signatures, callback processing, ZIP download, and media/font persistence through a storage adapter, and invoke site hooks.
Installation
composer require verstka/sdk
Requirements:
| Requirement | Value |
|---|---|
| PHP | 8.2+ |
| Extensions | ext-json, ext-hash, ext-zip |
| HTTP client | Guzzle 7 |
Configuration
use Verstka\Sdk\Config\VerstkaConfig; $config = new VerstkaConfig( apiKey: 'verstka-api-key', apiSecret: 'verstka-api-secret', callbackUrl: 'https://site.example/verstka/callback', apiUrl: 'https://api.r2.verstka.org/integration', maxContentSize: 200 * 1024 * 1024, requestTimeout: 60.0, downloadTimeout: 120.0, debug: false, );
apiUrl defaults to https://api.r2.verstka.org/integration.
maxContentSize defaults to 200 MiB when omitted.
Main methods
| Method | Purpose |
|---|---|
VerstkaClient::getEditorUrl(...) |
Opens a session via POST /session/open and returns the editor URL. |
VerstkaClient::processMaterialCallback(...) |
Handles article_saved: signature, ZIP, media, onFinalize. |
VerstkaClient::processFontsCallback(...) |
Handles site_fonts_updated: signature, fonts ZIP, font files, manifests. |
StorageAdapter::saveMedia(...) |
Saves a file from vms_media/* and returns a public URL. |
StorageAdapter::saveFontFile(...) |
Saves a font file and returns a public URL. |
StorageAdapter::saveFontsManifest(...) |
Saves vms_fonts.css or vms_fonts.json and returns a URL. |
LocalStorageAdapter |
Filesystem reference storage adapter. |
CallbackDispatcher::dispatch(...) |
Routes a single callback endpoint by event field. |
CallbackDispatcher::mapException(...) |
Maps SDK exceptions to HTTP error responses. |
SignatureService::signMaterial(...) |
Builds HMAC for material_id:url. |
SignatureService::verifySignature(...) |
Verifies HMAC. |
UrlBuilder::buildAuthorizedContentUrl(...) |
Adds api_key and material_id to content_url. |
Open editor
use Verstka\Sdk\Client\VerstkaClient; use Verstka\Sdk\Config\VerstkaConfig; $client = new VerstkaClient(new VerstkaConfig(...)); $editorUrl = $client->getEditorUrl( materialId: '42', vmsJson: $storedVmsJson, metadata: [ // optional: 'anySiteAdditionalKey' => 'anySiteAdditionalValue', // optional: 'timeLimitedAuthToken' => 'cms-scope-token', // optional: 'customContainers' => [], // optional: 'webhook_auth_user' => 'callback-user', // (see Callback Authorization) // optional: 'webhook_auth_password' => 'callback-password', ], );
Both vmsJson and metadata accept an array or a JSON string. The SDK sends
metadata as a JSON object and automatically adds
version: "php_<sdk-version>". For Basic Auth or a Bearer token, pass
webhook_auth_user and optionally webhook_auth_password in metadata when
calling getEditorUrl (see
Callback Authorization).
Sites usually pass timeLimitedAuthToken and customContainers, plus any site-specific keys (e.g. anySiteAdditionalKey: anySiteAdditionalValue).
Other custom keys are allowed — see
metadata in the API
docs.
Service keys: version_id, version_cdate, user_email, user_ip — Verstka
adds or updates them in the callback after save; webhook_auth_user and
webhook_auth_password authorize the outgoing callback and session-restore GET
requests to clientUrl (see
Callback Authorization).
Your callback handler usually ignores webhook_auth_*.
Open the editor in a separate tab:
<a href="<?= htmlspecialchars($getEditorUrlScript) ?>" target="_blank" rel="noopener noreferrer"> Edit in Verstka </a>
StorageAdapter
use Verstka\Sdk\Storage\StorageAdapter; final class CmsStorage implements StorageAdapter { public function saveMedia( string $filename, string $tempPath, string $materialId, array $metadata, ): string { return $this->saveToCdn("materials/$materialId/$filename", $tempPath); } public function saveFontFile( string $filename, string $tempPath, string $materialId, array $metadata, ): string { return $this->saveToCdn("verstka/fonts/$filename", $tempPath); } public function saveFontsManifest( string $filename, string $tempPath, string $materialId, array $metadata, ): string { return $this->saveToCdn("verstka/fonts/$filename", $tempPath); } }
Material callback
use Verstka\Sdk\Finalize\ContentFinalizeContext; use Verstka\Sdk\Finalize\ContentFinalizeResult; $result = $client->processMaterialCallback( callbackData: $requestPayload, signature: $request->headers->get('X-Verstka-Signature', ''), storage: $storage, onFinalize: function (ContentFinalizeContext $ctx): ContentFinalizeResult { ArticleRepository::saveVerstkaContent( materialId: $ctx->materialId, html: $ctx->vmsHtml, vmsJson: $ctx->vmsJson, metadata: $ctx->metadata, ); return new ContentFinalizeResult(success: true, vmsJson: $ctx->vmsJson); }, ); return json_response($result->toResponse());
ContentFinalizeContext contains materialId, metadata, vmsJson, vmsHtml,
and savedMediaUrls. By the time the hook runs, the SDK has already replaced
dummy-* URLs with public URLs from StorageAdapter.
Fonts callback
use Verstka\Sdk\Finalize\FontsFinalizeContext; use Verstka\Sdk\Finalize\FontsFinalizeResult; $result = $client->processFontsCallback( callbackData: $payload, signature: $signature, storage: $storage, onFinalize: function (FontsFinalizeContext $ctx): FontsFinalizeResult { SiteSettings::set('verstka_fonts_css_url', $ctx->cssUrl); SiteSettings::set('verstka_fonts_json_url', $ctx->jsonUrl); return new FontsFinalizeResult(success: true, fonts: $ctx->fonts); }, );
onFinalize for fonts may be omitted if saving files via storage and returning
the updated fonts tree to Verstka is enough.
PreSave hooks
processMaterialCallback and processFontsCallback accept an optional
onPreSave. It runs before ZIP download when your site needs extra validation
of Verstka callback requests.
use Verstka\Sdk\Finalize\ContentPreSaveContext; use Verstka\Sdk\Finalize\PreSaveDecision; $result = $client->processMaterialCallback( callbackData: $payload, signature: $signature, storage: $storage, onFinalize: $onFinalize, onPreSave: function (ContentPreSaveContext $ctx): PreSaveDecision { if (!UserPolicy::canSave($ctx->metadata['timeLimitedAuthToken'] ?? null, $ctx->materialId)) { return new PreSaveDecision(allow: false, reason: 'Access denied'); } return new PreSaveDecision(allow: true); }, );
If allow=false, the ZIP is not downloaded, no files are written, and Verstka
receives rc: 0.
Unified callback endpoint
Use one POST /verstka/callback route for both material and fonts events.
CallbackDispatcher is the same helper used inside Laravel and Symfony controllers:
use Verstka\Sdk\Integration\CallbackDispatcher; use Verstka\Sdk\Finalize\ContentFinalizeContext; use Verstka\Sdk\Finalize\ContentFinalizeResult; use Verstka\Sdk\Finalize\FontsFinalizeContext; use Verstka\Sdk\Finalize\FontsFinalizeResult; try { $response = CallbackDispatcher::dispatch( client: $client, payload: $requestPayload, signature: $request->headers->get('X-Verstka-Signature', ''), storage: $storage, onContentFinalize: function (ContentFinalizeContext $ctx): ContentFinalizeResult { ArticleRepository::saveVerstkaContent( materialId: $ctx->materialId, html: $ctx->vmsHtml, vmsJson: $ctx->vmsJson, metadata: $ctx->metadata, ); return new ContentFinalizeResult(success: true, vmsJson: $ctx->vmsJson); }, onFontsFinalize: function (FontsFinalizeContext $ctx): FontsFinalizeResult { SiteSettings::set('verstka_fonts_css_url', $ctx->cssUrl); return new FontsFinalizeResult(success: true, fonts: $ctx->fonts); }, ); return json_response($response); } catch (\Throwable $e) { $error = CallbackDispatcher::mapException($e); return json_response($error->toArray(), $error->status); }
The dispatcher reads $payload['event']: site_fonts_updated routes to the fonts
flow, everything else to the material flow.
Symfony and Laravel
| Framework | What the SDK provides |
|---|---|
| Symfony | Bundle, DI configuration, CallbackController, exception subscriber. |
| Laravel | Service provider, config publish, auto route POST /verstka/callback, exception renderer. |
Framework adapters register a single callback endpoint and call
CallbackDispatcher internally — same routing by event.
Documentation
Full integration guide (Russian): frontend/docs/ru/dev/sdk-php.md
Related: API integration, site integration.
License
MIT — see LICENSE.
Opening a legacy article
Requires a compatible backend/editor with legacy import enabled. The SDK submits HTML in a single session/open POST and requires legacy_import_accepted: true with a valid legacy_attempt_id in the response. It reports errors without retrying as a blank article. Ordinary JSON takes precedence when both inputs are supplied.
$url = $client->getEditorUrl('article-123', legacyArticle: [ 'desktop_html' => $desktopHtml, 'mobile_html' => $mobileHtml, 'mobile_breakpoint' => 768, 'images_hostname' => 'https://old.example.org', 'fontscss_url' => 'https://old.example.org/fonts.css', ]);
Supply at least one HTML (maximum 5 MiB UTF-8 each); a pair requires a positive breakpoint. Media origins and font CSS are optional when not needed. Resource preparation runs on the backend as the editor opens, with progress. Concurrent requests for the same active material attach to its current attempt and retain its first input. After preparation finishes, a new legacy request reuses the session and processes the latest HTML again, even if unchanged. During an in-flight Save, replacement can return 409 session_busy; retry after Save completes. Opening does not publish. Normal Save delivers fonts through site_fonts_updated before article_saved, using the existing callback processors.