Portal Webhooks
Subscribe to recipe, creative asset, Smart List, and review batch 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, a Smart List's membership updates, or a review batch is decided.
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 |
review_batch.approved | A review batch in Operations Lab is approved |
review_batch.declined | A review batch in Operations Lab is declined |
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.
review_batch.approved
Sent once when a review batch is approved, by a reviewer or by the steps of its workflow.
{
"event": "review_batch.approved",
"timestamp": "2026-10-29T12:04:51.210Z",
"data": {
"reviewBatchId": "a7209ec8-de09-4c6f-a048-e4f7f82e34a6",
"reviewBatchNumber": 7,
"ruleName": "Weekly binge markers check",
"catalogId": 196,
"catalogName": "my_catalog",
"products": [
{"product": "contextual_adbreak", "version": "15"},
{"product": "markers", "version": "27"}
],
"decidedAt": "2026-10-29T12:04:50.981Z",
"declineReason": null,
"assets": [
{"assetId": "317604421cfcaf61", "inventoryId": "movie-1042", "decision": "approved"},
{"assetId": "fff926e53030b33e", "inventoryId": "movie-1043", "decision": "declined"}
]
}
}| Field | Description |
|---|---|
reviewBatchId | Review batch ID |
reviewBatchNumber | Number the app shows next to the batch |
ruleName | Batch rule that created the batch |
products | Vionlabs products and versions the batch reviewed |
declineReason | Why the batch was declined, or null |
assets | Every asset of the batch: Vionlabs asset ID, your inventory ID, decision |
An approved batch can still list declined assets: a reviewer declined them before the batch was approved.
review_batch.declined uses the same data shape. Every asset of a declined batch is declined.
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.