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
README
PHP SDK for Verstka API v2. Open editor sessions, verify and process callbacks (material content + site fonts), and plug the result into Symfony or Laravel.
How it works
The SDK is a bridge between your CMS and the Verstka visual editor. It handles two main flows:
- Open the editor — your backend requests a session URL for a specific material.
- Receive the result — Verstka POSTs a signed callback to your
callback_urlafter the user saves.
Your CMS → SDK (getEditorUrl) → Verstka API → editor URL
User edits in Verstka
Verstka → POST /your/callback → SDK (process*Callback) → your DB + storage
The SDK does not persist business data for you. It handles API communication, signature verification, ZIP download/extraction, media URL patching, and file storage via StorageAdapter. You provide the callback endpoint, storage, and hooks to save HTML/JSON into your database.
Features
- Sync
VerstkaClienton Guzzle. - Framework-agnostic core with optional Symfony bundle and Laravel service provider.
- HMAC-SHA256 signature verification for incoming callbacks.
- Streaming ZIP download with a configurable size cap and path-traversal protection.
- Automatic extraction of
vms_media/*,vms_json.json,vms_html.html, and font bundles. - Automatic
dummy-*replacement in HTML/CSS andclientUrlupdates insidevms_json.assetsand the fonts tree. - Storage adapter interface with a reference filesystem implementation.
- Optional pre-save and finalize hooks for access control and CMS persistence.
What you need to integrate
Requirements
composer require verstka/sdk
- PHP 8.2+
- Extensions:
ext-json,ext-hash,ext-zip - Guzzle (installed automatically)
Optional framework integrations:
composer require verstka/sdk symfony/framework-bundle # Symfony composer require verstka/sdk laravel/framework # Laravel
Verstka credentials
| Parameter | Purpose |
|---|---|
apiKey |
Identifies your project in Verstka |
apiSecret |
Signs outgoing requests and verifies incoming callbacks |
callbackUrl |
Public URL of your callback endpoint (must be reachable by Verstka) |
What you implement in your project
| Piece | Responsibility |
|---|---|
| Callback endpoint | Public route that receives Verstka POSTs (e.g. POST /verstka/callback) |
StorageAdapter |
Where media and font files are stored (disk, S3, CDN, etc.) |
onFinalize hook |
Save vmsHtml / vmsJson (or font metadata) into your CMS database |
| “Edit in Verstka” UI | Call getEditorUrl() and open the returned URL for the user |
Configuration
use Verstka\Sdk\Config\VerstkaConfig; $config = new VerstkaConfig( apiKey: 'verstka-api-key', apiSecret: 'verstka-api-secret', callbackUrl: 'https://app.example.com/verstka/callback', apiUrl: 'https://api.r2.verstka.org/integration', maxContentSize: 100 * 1024 * 1024, requestTimeout: 60.0, downloadTimeout: 120.0, basicAuthUser: null, basicAuthPassword: null, debug: false, );
Quickstart
Open an editor session
use Verstka\Sdk\Client\VerstkaClient; use Verstka\Sdk\Config\VerstkaConfig; $client = new VerstkaClient(new VerstkaConfig(...)); $url = $client->getEditorUrl( materialId: '42', vmsJson: ['blocks' => []], metadata: [ 'userId' => 11, 'user_email' => 'user@example.com', 'user_ip' => '127.0.0.1', 'AnyOtherKey' => 'value', ], );
Both vmsJson and metadata accept either an array or a JSON string.
The SDK returns a URL string only — how you open it is up to your UI. Opening the editor in a new tab or window is the recommended approach:
- Verstka is a full-screen standalone editor, not a typical admin form embedded in your layout.
- The user stays in your CMS while the editor runs separately.
- After save, Verstka sends a server-side callback to your backend; the browser window can simply be closed.
HTML example:
<a href="{{ $editorUrl }}" target="_blank" rel="noopener noreferrer"> Edit in Verstka </a>
JavaScript example:
window.open(editorUrl, '_blank', 'noopener,noreferrer');
Process a material callback
use Verstka\Sdk\Finalize\ContentFinalizeContext; use Verstka\Sdk\Finalize\ContentFinalizeResult; use Verstka\Sdk\Storage\LocalStorageAdapter; $storage = new LocalStorageAdapter('/var/www/storage', 'https://cdn.example.com'); $result = $client->processMaterialCallback( $request->all(), signature: $request->header('X-Verstka-Signature', ''), storage: $storage, onFinalize: function (ContentFinalizeContext $ctx): ContentFinalizeResult { // Persist $ctx->vmsHtml / $ctx->vmsJson in your CMS return new ContentFinalizeResult(success: true, vmsJson: $ctx->vmsJson); }, ); return response()->json($result->toResponse());
Process a fonts callback
$result = $client->processFontsCallback( $payload, signature: $signature, storage: $storage, onFinalize: null, // optional — SDK still persists fonts via storage );
Callback hooks
The SDK exposes four hooks — two per callback type. They answer different questions at different stages of processing.
| Hook | When | Question it answers |
|---|---|---|
onPreSave |
Before ZIP download | “Should this operation proceed?” |
onFinalize |
After file processing | “Should the result be persisted in my CMS?” |
Material callbacks: onContentPreSave + onContentFinalize
Fonts callbacks: onFontsPreSave + onFontsFinalize
In Laravel/Symfony, all four are wired through the VerstkaCallbacks class.
Material callback flow
1. Verify X-Verstka-Signature
2. onPreSave (optional) ← reject before any download
3. Download ZIP, extract files
4. StorageAdapter::saveMedia() ← SDK saves files automatically
5. Patch dummy-* URLs in HTML/JSON
6. onFinalize (required) ← your CMS persistence logic
7. Return { rc, rm, data } to Verstka
onPreSave — gate before heavy work
Called immediately after signature verification, before the ZIP is downloaded. This is a cheap checkpoint to reject a callback early.
ContentPreSaveContext fields:
| Field | Description |
|---|---|
materialId |
Material ID in your CMS |
metadata |
Metadata you passed when opening the editor session |
contentUrl |
Verstka ZIP URL (not yet downloaded) |
FontsPreSaveContext fields: same as above, plus fonts — the font tree from the callback payload.
Return PreSaveDecision:
new PreSaveDecision(allow: true); new PreSaveDecision(allow: false, reason: 'Access denied');
If allow is false:
- the ZIP is not downloaded;
- files are not saved;
- Verstka receives
rc: 0and yourreason(or"Operation rejected"by default).
Typical use cases:
- Permission checks (
metadata['userId']cannot edit this material). - Publication state (published articles require an editor role).
- Edit locks (another user is already editing).
- Storage quotas (skip download when over limit).
- Metadata validation (unexpected or missing fields).
onFinalize — persist structured data in your CMS
Called after the SDK has:
- downloaded and extracted the ZIP;
- saved media/font files via
StorageAdapter; - replaced
dummy-*placeholders with real public URLs in HTML/CSS/JSON.
ContentFinalizeContext fields (required hook for material callbacks):
| Field | Description |
|---|---|
materialId |
Material ID |
metadata |
Session metadata from your CMS |
vmsJson |
Parsed vms_json (or null) |
vmsHtml |
Ready-to-use HTML (or null) |
savedMediaUrls |
['image.jpg' => 'https://cdn.../image.jpg', ...] |
Return ContentFinalizeResult:
new ContentFinalizeResult(success: true, vmsJson: $ctx->vmsJson); new ContentFinalizeResult(success: false);
If success is false, Verstka receives rc: 0 with message "Operation failed".
Typical use cases:
- Write
vmsHtmlandvmsJsonto your articles table. - Create a revision before overwriting.
- Post-process HTML, generate excerpts, update search indexes.
- Update
updated_at, editor ID, draft status. - Wrap DB writes in a transaction; return
success: falseon failure.
Note:
onFinalizeis for structured CMS data. Binary files (images, fonts) are already saved byStorageAdapterbefore this hook runs.
onFontsFinalize — optional
For font callbacks, onFinalize is optional (null by default). Without it, the SDK still:
- saves font files via
StorageAdapter; - updates
vms_fonts.cssandvms_fonts.json; - sets
clientUrlin the font tree.
FontsFinalizeContext fields:
| Field | Description |
|---|---|
materialId |
Material ID |
metadata |
Session metadata |
fonts |
Font tree with clientUrl already set |
cssUrl |
Public URL of vms_fonts.css |
jsonUrl |
Public URL of vms_fonts.json |
savedFontUrls |
['font-id' => 'https://cdn.../font.woff2', ...] |
Use onFontsFinalize when you need to:
- store font references in site settings;
- invalidate CDN cache;
- audit who updated fonts;
- return a modified
fontstree in the Verstka response.
StorageAdapter vs hooks
| Responsibility | Who handles it | When |
|---|---|---|
| Save media/font files | SDK via StorageAdapter |
Between preSave and finalize |
| Access control | You via onPreSave |
Before ZIP download |
| Save HTML/JSON to DB | You via onFinalize |
After file processing |
StorageAdapter decides where files go (disk, S3).
onFinalize decides where structured content goes (MySQL, Elasticsearch, etc.).
Full example with both hooks
use Verstka\Sdk\Finalize\ContentFinalizeContext; use Verstka\Sdk\Finalize\ContentFinalizeResult; use Verstka\Sdk\Finalize\ContentPreSaveContext; use Verstka\Sdk\Finalize\PreSaveDecision; $result = $client->processMaterialCallback( $payload, signature: $signature, storage: $storage, onPreSave: function (ContentPreSaveContext $ctx): PreSaveDecision { $userId = $ctx->metadata['userId'] ?? null; if (!$userId || !User::canEdit($userId, $ctx->materialId)) { return new PreSaveDecision(allow: false, reason: 'Access denied'); } return new PreSaveDecision(allow: true); }, onFinalize: function (ContentFinalizeContext $ctx): ContentFinalizeResult { try { Article::updateOrCreate( ['id' => $ctx->materialId], [ 'html' => $ctx->vmsHtml, 'json' => json_encode($ctx->vmsJson), ], ); return new ContentFinalizeResult(success: true, vmsJson: $ctx->vmsJson); } catch (\Throwable) { return new ContentFinalizeResult(success: false); } }, );
Hook reference
| Hook | Required? | Before/after ZIP | Your decision |
|---|---|---|---|
onContentPreSave |
No | Before | Allow or reject the callback |
onContentFinalize |
Yes | After | Persist HTML/JSON in CMS |
onFontsPreSave |
No | Before | Allow or reject font update |
onFontsFinalize |
No | After | Persist font settings in CMS |
Rendering saved articles
This SDK processes editor callbacks, persists media/fonts through your storage
adapter, and gives your backend the rewritten vms_html and vms_json. It does
not initialize the article in the browser by itself.
To display saved Verstka articles on your site, use the frontend viewer package
verstka-viewer, or implement
the same initialization/data-handling behavior in your own frontend using the
information from that package.
npm install verstka-viewer
Serve the saved vms_html together with the matching vms_json that you
persisted in onFinalize; media and font URLs should be the public URLs
returned by your StorageAdapter.
Storage adapters
Implement Verstka\Sdk\Storage\StorageAdapter or use LocalStorageAdapter:
$storage = new LocalStorageAdapter( root: '/var/www/storage', baseUrl: 'https://cdn.example.com', );
Symfony integration
- Register the bundle (auto-discovered via
composer.jsonextra). - Configure
config/packages/verstka.yaml:
verstka: api_key: '%env(VERSTKA_API_KEY)%' api_secret: '%env(VERSTKA_API_SECRET)%' callback_url: '%env(VERSTKA_CALLBACK_URL)%' callback_route_prefix: /verstka
- Register your
StorageAdapterand optionalVerstkaCallbacksservices. - Import routes from
vendor/verstka/sdk/src/Verstka/Sdk/Integration/Symfony/Resources/config/routes.yaml.
POST /verstka/callback handles both material and site_fonts_updated events.
Laravel integration
- Publish config:
php artisan vendor:publish --tag=verstka-config - Set env vars:
VERSTKA_API_KEY,VERSTKA_API_SECRET,VERSTKA_CALLBACK_URL. - Bind
Verstka\Sdk\Storage\StorageAdapterand optionalVerstkaCallbacksin a service provider.
The service provider registers POST /verstka/callback automatically.
Low-level helpers
use Verstka\Sdk\Signature\SignatureService; use Verstka\Sdk\Url\UrlBuilder; SignatureService::signMaterial($materialId, $url, $secret); SignatureService::verifySignature($materialId, $url, $signature, $secret); UrlBuilder::buildAuthorizedContentUrl($contentUrl, $apiKey, $materialId);
Python SDK parity
This package mirrors the sync subset of verstka-sdk (Python):
| Python | PHP |
|---|---|
VerstkaConfig |
Verstka\Sdk\Config\VerstkaConfig |
VerstkaClient |
Verstka\Sdk\Client\VerstkaClient |
CallbackProcessor |
Verstka\Sdk\Callback\CallbackProcessor |
sign_material |
SignatureService::signMaterial |
build_authorized_content_url |
UrlBuilder::buildAuthorizedContentUrl |
LocalStorageAdapter |
Verstka\Sdk\Storage\LocalStorageAdapter |
FastAPI build_callback_router |
Symfony CallbackController / Laravel route |
Releasing
Publishing to packagist.org is automated via .github/workflows/publish.yml when a v* tag is pushed.
One-time setup
- Submit the repository at packagist.org/packages/submit (
https://github.com/verstka/verstka-sdk-php). - Confirm repository ownership on Packagist.
- Add GitHub Actions secrets in the repository settings:
PACKAGIST_USERNAME— your packagist.org usernamePACKAGIST_TOKEN— API token from packagist.org/profile
Do not enable the Packagist GitHub webhook if you rely on the publish workflow — the API update runs only after tests pass.
Release a version
git tag v0.1.0 git push origin v0.1.0
The workflow validates composer.json, runs PHPUnit, then calls the Packagist update-package API.
Verify installation:
composer require verstka/sdk:0.1.0
License
MIT — see LICENSE.