Portal Webhooks
Subscribe to recipe, creative asset, and Smart List events from the Vionlabs app
Portal webhooks notify your systems when work in the Vionlabs app changes: a recipe finishes, a creative asset is ready, or a Smart List's membership updates.
You subscribe in the app. No Vionlabs-side setup ticket is required.
This is separate from processing webhooks, which notify you when catalog asset processing completes.
Subscribe
- Open app.vionlabs.com → Settings → Webhooks
- Click Create Webhook
- Enter the HTTPS URL that should receive events
- Select the events you want
- Copy the signing secret (shown only once)
- Enable the webhook
Use Test on an existing webhook to send a sample payload to your endpoint.
Events
| Event | When it fires |
|---|---|
recipe.completed | A published recipe finishes successfully |
recipe.failed | A published recipe fails |
creative_asset.completed | A creative asset is generated and ready |
creative_asset.created | A creative asset record is created (files may not be ready yet) |
creative_asset.failed | Creative asset generation fails |
smart_list.updated | A Smart List's membership changes |
smart_list.updated fires when titles are added or removed from a list. The usual case is the daily license-window refresh: titles that become active are added, and titles whose license has expired are removed.
Payload format
Every event uses the same envelope:
{
"event": "smart_list.updated",
"timestamp": "2025-12-29T02:15:02.693Z",
"data": {}
}timestamp is ISO 8601 UTC. data depends on the event.
smart_list.updated
{
"event": "smart_list.updated",
"timestamp": "2025-12-29T02:15:02.693Z",
"data": {
"smartListId": "9f6555a6-01ad-4d1b-9f99-82a18066e2d8",
"smartListTitle": "Currently Licensed Movies",
"addedAssetIds": ["317604421cfcaf61"],
"removedAssetIds": ["fff926e53030b33e"]
}
}| Field | Description |
|---|---|
smartListId | Smart List ID (same ID as in the Portal API) |
smartListTitle | Current list title |
addedAssetIds | Vionlabs asset IDs that joined the list |
removedAssetIds | Vionlabs asset IDs that left the list |
Either array can be empty. After you receive this event, GET /smartlists/{id}/assets returns the current membership.
recipe.completed
{
"event": "recipe.completed",
"timestamp": "2025-12-29T10:15:02.693Z",
"data": {
"recipeId": "9f6555a6-01ad-4d1b-9f99-82a18066e2d8",
"recipeTitle": "My new recipe",
"status": "completed",
"dataOnly": false,
"creativeAssetCount": 4,
"creativeAssetIds": [
"c1dc10a0-8bdf-4e2b-9d45-4e46fb03a98d",
"f89cae58-9a4e-4cde-9a85-007be27cf974"
]
}
}recipe.failed
{
"event": "recipe.failed",
"timestamp": "2025-12-29T10:15:02.693Z",
"data": {
"recipeId": "9f6555a6-01ad-4d1b-9f99-82a18066e2d8",
"recipeTitle": "My new recipe",
"status": "failed",
"dataOnly": false,
"errorMessage": "All assets failed during JIT API calls"
}
}creative_asset.completed
{
"event": "creative_asset.completed",
"timestamp": "2025-12-29T10:15:19.658Z",
"data": {
"creativeAssetId": "c1dc10a0-8bdf-4e2b-9d45-4e46fb03a98d",
"assetId": "317604421cfcaf61",
"recipeId": "9f6555a6-01ad-4d1b-9f99-82a18066e2d8",
"recipeTitle": "My new recipe",
"type": "thumbnail-vertical",
"url": "317604421cfcaf61_17-vertical-logo/V6VBrT/thumbnail_43355.jpg"
}
}creative_asset.created and creative_asset.failed use the same data shape. url may be null when files are not ready yet.
Headers
Each delivery is an HTTP POST with Content-Type: application/json and these headers:
| Header | Description |
|---|---|
X-Vionlabs-Signature | HMAC SHA-256 hex digest of the raw request body, signed with your webhook secret |
X-Vionlabs-Timestamp | ISO 8601 time the request was sent |
X-Vionlabs-Event | Event type, for example smart_list.updated. Omitted on Test deliveries. |
Verify the signature
Compute HMAC SHA-256 over the raw request body (the exact bytes you received, not a re-serialized JSON object) using the secret shown when you created the webhook. Compare it to X-Vionlabs-Signature with a timing-safe comparison.
X-Vionlabs-Timestamp is informational and is not part of the HMAC. If you need freshness or deduplication, use the signed timestamp field in the JSON body.
const crypto = require('crypto')
function verifyWebhookSignature(rawBody, signature, secret) {
if (typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) {
return false
}
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))
}Delivery
- Method:
POST - Timeout: 30 seconds
- A
2xxresponse marks the delivery as successful - Failed deliveries are not retried today (single attempt)
Respond quickly. Do heavier work (Portal API fetches, downstream updates) after you return 200.
Test event
The Test action sends this payload. It is not a real product event:
{
"event": "test",
"timestamp": "2025-12-29T10:15:02.693Z",
"data": {
"message": "This is a test webhook from VionLabs"
}
}The test request includes X-Vionlabs-Signature and X-Vionlabs-Timestamp, but not X-Vionlabs-Event.