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: 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:

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?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: 'team@company.com',
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