Skip to main content

Webhooks

Receive real-time notifications when assets are added, updated, or deleted in your Playbook organization.

Overview​

Webhooks (also called triggers) allow you to integrate Playbook with external systems by receiving HTTP callbacks when specific events occur. This enables:

  • Real-time synchronization with other platforms
  • Automated workflows triggered by asset changes
  • Custom notifications and alerts
  • Analytics and audit logging

Prerequisites​

  • Access Token: API token with webhook management permissions
  • Webhook URL: Your server endpoint to receive webhook payloads
  • Organization Slug: Your organization identifier
  • Board Token: The board you want to monitor

Available Events​

Playbook supports three webhook event types:

EventTriggerPayload
asset_addedNew asset createdFull asset object
asset_updatedAsset modifiedUpdated asset object
asset_deletedAsset removedAsset ID and token

Creating a Webhook​

Basic Webhook Setup​

curl -X POST "https://api.playbook.com/v1/trigger" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"hook": "asset_added",
"hook_url": "https://your-server.com/webhooks/playbook",
"organization_slug": "my-org",
"collection_token": "main-board"
}'

Response:

{
"id": "trigger-abc123def",
"expiration_date": "2025-01-09T18:00:00Z"
}

Note: Webhooks expire after a set period. You'll need to recreate them periodically.


Webhook Payload Format​

Asset Added Event​

{
"event": "asset_added",
"timestamp": "2024-10-09T18:30:00Z",
"organization": {
"slug": "my-org",
"name": "My Organization"
},
"collection": {
"token": "main-board",
"title": "Main Board"
},
"asset": {
"id": 12345,
"token": "new-asset-xyz",
"title": "Product Photo",
"media_type": "image/jpeg",
"display_url": "https://cdn.playbook.com/product-photo.jpg",
"created_at": "2024-10-09T18:30:00Z",
"updated_at": "2024-10-09T18:30:00Z",
"tags": ["product", "photography"],
"fields": {
"Approval Status": "Draft"
}
}
}

Asset Updated Event​

{
"event": "asset_updated",
"timestamp": "2024-10-09T19:00:00Z",
"organization": {
"slug": "my-org",
"name": "My Organization"
},
"collection": {
"token": "main-board",
"title": "Main Board"
},
"asset": {
"id": 12345,
"token": "new-asset-xyz",
"title": "Product Photo - Final",
"updated_at": "2024-10-09T19:00:00Z",
"changes": {
"title": {
"old": "Product Photo",
"new": "Product Photo - Final"
},
"fields": {
"old": { "Approval Status": "Draft" },
"new": { "Approval Status": "Approved" }
}
}
}
}

Asset Deleted Event​

{
"event": "asset_deleted",
"timestamp": "2024-10-09T20:00:00Z",
"organization": {
"slug": "my-org",
"name": "My Organization"
},
"collection": {
"token": "main-board",
"title": "Main Board"
},
"asset": {
"id": 12345,
"token": "new-asset-xyz",
"title": "Product Photo - Final"
}
}

Receiving Webhooks​

Basic Server Example (Node.js/Express)​

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

app.post('/webhooks/playbook', async (req, res) => {
const { event, asset, collection, organization } = req.body;

console.log(`Received ${event} event for asset: ${asset.token}`);

// Process the webhook
try {
await handleWebhookEvent(event, asset, collection, organization);

// Always respond quickly with 200
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);

// Still return 200 to prevent retries
res.status(200).json({ received: true, error: error.message });
}
});

async function handleWebhookEvent(event, asset, collection, organization) {
switch (event) {
case 'asset_added':
await onAssetAdded(asset, collection);
break;
case 'asset_updated':
await onAssetUpdated(asset, collection);
break;
case 'asset_deleted':
await onAssetDeleted(asset, collection);
break;
}
}

async function onAssetAdded(asset, collection) {
console.log(`New asset added: ${asset.title}`);
// Your logic here
}

async function onAssetUpdated(asset, collection) {
console.log(`Asset updated: ${asset.title}`);
// Your logic here
}

async function onAssetDeleted(asset, collection) {
console.log(`Asset deleted: ${asset.token}`);
// Your logic here
}

app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Python (Flask) Example​

from flask import Flask, request, jsonify
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

@app.route('/webhooks/playbook', methods=['POST'])
def handle_webhook():
payload = request.get_json()

event = payload.get('event')
asset = payload.get('asset')
collection = payload.get('collection')

logging.info(f"Received {event} for asset: {asset.get('token')}")

try:
if event == 'asset_added':
handle_asset_added(asset, collection)
elif event == 'asset_updated':
handle_asset_updated(asset, collection)
elif event == 'asset_deleted':
handle_asset_deleted(asset, collection)

return jsonify({'received': True}), 200
except Exception as e:
logging.error(f"Error processing webhook: {e}")
return jsonify({'received': True, 'error': str(e)}), 200

def handle_asset_added(asset, collection):
print(f"New asset: {asset['title']}")
# Your logic here

def handle_asset_updated(asset, collection):
print(f"Updated asset: {asset['title']}")
# Your logic here

def handle_asset_deleted(asset, collection):
print(f"Deleted asset: {asset['token']}")
# Your logic here

if __name__ == '__main__':
app.run(port=3000)

Deleting Webhooks​

Remove webhooks when they're no longer needed:

curl -X DELETE "https://api.playbook.com/v1/trigger/trigger-abc123def" \
-H "Authorization: Bearer YOUR_TOKEN"

Response: HTTP 204 No Content


Use Cases​

1. Sync with External CMS​

Keep your CMS in sync with Playbook assets:

async function onAssetAdded(asset, collection) {
// Add asset reference to CMS
await cmsAPI.createAsset({
id: asset.token,
title: asset.title,
url: asset.display_url,
source: 'playbook',
collectionId: collection.token
});
}

async function onAssetUpdated(asset, collection) {
// Update CMS asset
await cmsAPI.updateAsset(asset.token, {
title: asset.title,
tags: asset.tags,
metadata: asset.fields
});
}

async function onAssetDeleted(asset, collection) {
// Remove from CMS
await cmsAPI.deleteAsset(asset.token);
}

2. Approval Notifications​

Send notifications when assets are approved:

async function onAssetUpdated(asset, collection) {
// Check if approval status changed to approved
if (asset.changes?.fields?.new?.['Approval Status'] === 'Approved') {
await sendNotification({
subject: `Asset Approved: ${asset.title}`,
body: `The asset "${asset.title}" has been approved and is ready for use.`,
assetUrl: asset.display_url
});
}
}

3. Analytics Tracking​

Track asset lifecycle for analytics:

async function handleWebhookEvent(event, asset, collection, organization) {
// Log to analytics service
await analyticsAPI.track({
event: `playbook_${event}`,
properties: {
assetId: asset.token,
assetTitle: asset.title,
mediaType: asset.media_type,
collection: collection.token,
organization: organization.slug,
tags: asset.tags
},
timestamp: new Date()
});
}

4. Automated Backup​

Create backups when assets are added:

async function onAssetAdded(asset, collection) {
if (asset.media_type.startsWith('image/') || asset.media_type.startsWith('video/')) {
// Queue backup job
await backupQueue.add({
assetToken: asset.token,
sourceUrl: asset.display_url,
backupPath: `backups/${organization}/${collection.token}/${asset.token}`,
metadata: {
title: asset.title,
createdAt: asset.created_at
}
});
}
}

Best Practices​

1. Respond Quickly​

Always respond with 200 OK immediately, then process asynchronously:

app.post('/webhooks/playbook', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });

// Process asynchronously
setImmediate(async () => {
try {
await processWebhook(req.body);
} catch (error) {
console.error('Async webhook processing failed:', error);
}
});
});

2. Implement Idempotency​

Handle duplicate deliveries gracefully:

const processedEvents = new Set();

async function processWebhook(payload) {
const eventId = `${payload.event}_${payload.asset.token}_${payload.timestamp}`;

if (processedEvents.has(eventId)) {
console.log('Duplicate event, skipping');
return;
}

processedEvents.add(eventId);

// Process the event
await handleWebhookEvent(payload);

// Clean up old IDs periodically
if (processedEvents.size > 1000) {
processedEvents.clear();
}
}

3. Handle Failures Gracefully​

async function handleWebhookEvent(event, asset) {
try {
await processEvent(event, asset);
} catch (error) {
console.error(`Failed to process ${event}:`, error);

// Log to error tracking service
await errorTracker.captureException(error, {
extra: {
event,
assetToken: asset.token,
timestamp: new Date()
}
});

// Queue for retry
await retryQueue.add({
event,
asset,
attemptCount: 1
});
}
}

4. Validate Webhook Source​

Verify webhooks are from Playbook (implement based on your security requirements):

function validateWebhook(req) {
// Check IP allowlist
const allowedIPs = ['52.12.34.56', '52.12.34.57'];
const requestIP = req.ip;

if (!allowedIPs.includes(requestIP)) {
throw new Error('Invalid webhook source');
}

// Verify payload structure
const { event, asset, organization } = req.body;

if (!event || !asset || !organization) {
throw new Error('Invalid payload structure');
}

return true;
}

app.post('/webhooks/playbook', async (req, res) => {
try {
validateWebhook(req);
await processWebhook(req.body);
res.status(200).json({ received: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});

Complete Integration Example​

Here's a complete webhook integration with a database:

const express = require('express');
const { MongoClient } = require('mongodb');

class PlaybookWebhookHandler {
constructor(mongoUrl, dbName) {
this.mongoUrl = mongoUrl;
this.dbName = dbName;
this.client = null;
this.db = null;
}

async connect() {
this.client = await MongoClient.connect(this.mongoUrl);
this.db = this.client.db(this.dbName);
console.log('Connected to MongoDB');
}

async handleAssetAdded(payload) {
const { asset, collection, organization } = payload;

await this.db.collection('assets').insertOne({
playbookId: asset.token,
title: asset.title,
mediaType: asset.media_type,
displayUrl: asset.display_url,
collection: collection.token,
organization: organization.slug,
tags: asset.tags,
fields: asset.fields,
createdAt: new Date(asset.created_at),
syncedAt: new Date()
});

console.log(`Synced new asset: ${asset.title}`);
}

async handleAssetUpdated(payload) {
const { asset, collection } = payload;

await this.db.collection('assets').updateOne(
{ playbookId: asset.token },
{
$set: {
title: asset.title,
tags: asset.tags,
fields: asset.fields,
updatedAt: new Date(asset.updated_at),
syncedAt: new Date()
}
}
);

console.log(`Synced updated asset: ${asset.title}`);
}

async handleAssetDeleted(payload) {
const { asset } = payload;

await this.db.collection('assets').updateOne(
{ playbookId: asset.token },
{
$set: {
deleted: true,
deletedAt: new Date(),
syncedAt: new Date()
}
}
);

console.log(`Marked asset as deleted: ${asset.token}`);
}

async processWebhook(payload) {
const { event } = payload;

// Log the event
await this.db.collection('webhook_logs').insertOne({
event,
payload,
receivedAt: new Date()
});

// Handle based on event type
switch (event) {
case 'asset_added':
await this.handleAssetAdded(payload);
break;
case 'asset_updated':
await this.handleAssetUpdated(payload);
break;
case 'asset_deleted':
await this.handleAssetDeleted(payload);
break;
}
}
}

// Express server setup
const app = express();
app.use(express.json());

const handler = new PlaybookWebhookHandler(
'mongodb://localhost:27017',
'playbook_sync'
);

handler.connect();

app.post('/webhooks/playbook', async (req, res) => {
res.status(200).json({ received: true });

setImmediate(async () => {
try {
await handler.processWebhook(req.body);
} catch (error) {
console.error('Webhook processing failed:', error);
}
});
});

app.listen(3000);

Troubleshooting​

Webhooks Not Received​

Check your webhook URL:

  • Is it publicly accessible?
  • Does it use HTTPS?
  • Is the server running?

Verify the trigger is active:

# List all your triggers (if such endpoint exists)
# Or recreate if expired

Duplicate Events​

This is normal - implement idempotency:

// Track processed events
const cache = new Map();

function isDuplicate(eventId) {
if (cache.has(eventId)) {
return true;
}
cache.set(eventId, Date.now());
return false;
}

Missing Payload Data​

Check the API version - payload structure may vary.



Next Steps​