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: OAuth2 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:
| Event | Trigger | Payload |
|---|---|---|
asset_added | New asset created | Full asset object |
asset_updated | Asset modified | Updated asset object |
asset_deleted | Asset removed | Asset ID and token |
Creating a Webhook
Basic Webhook Setup
curl -X POST "https://api.playbook.com/v1/trigger?access_token=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?access_token=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({
to: '[email protected]',
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.
Related API Endpoints
Next Steps
- Learn about Custom Fields to add webhook triggers on field changes
- Explore Asset Management to automate workflows
- Read about Search to find assets programmatically