# Webhooks

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

## Overview[​](#overview "Direct link to 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[​](#prerequisites "Direct link to 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[​](#available-events "Direct link to 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[​](#creating-a-webhook "Direct link to Creating a Webhook")

### Basic Webhook Setup[​](#basic-webhook-setup "Direct link to 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[​](#webhook-payload-format "Direct link to Webhook Payload Format")

### Asset Added Event[​](#asset-added-event "Direct link to 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[​](#asset-updated-event "Direct link to 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[​](#asset-deleted-event "Direct link to 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[​](#receiving-webhooks "Direct link to Receiving Webhooks")

### Basic Server Example (Node.js/Express)[​](#basic-server-example-nodejsexpress "Direct link to 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[​](#python-flask-example "Direct link to 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[​](#deleting-webhooks "Direct link to 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[​](#use-cases "Direct link to Use Cases")

### 1. Sync with External CMS[​](#1-sync-with-external-cms "Direct link to 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[​](#2-approval-notifications "Direct link to 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[​](#3-analytics-tracking "Direct link to 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[​](#4-automated-backup "Direct link to 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[​](#best-practices "Direct link to Best Practices")

### 1. Respond Quickly[​](#1-respond-quickly "Direct link to 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[​](#2-implement-idempotency "Direct link to 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[​](#3-handle-failures-gracefully "Direct link to 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[​](#4-validate-webhook-source "Direct link to 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[​](#complete-integration-example "Direct link to 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[​](#troubleshooting "Direct link to Troubleshooting")

### Webhooks Not Received[​](#webhooks-not-received "Direct link to 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[​](#duplicate-events "Direct link to 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[​](#missing-payload-data "Direct link to Missing Payload Data")

Check the API version - payload structure may vary.

***

## Related API Endpoints[​](#related-api-endpoints "Direct link to Related API Endpoints")

* [Create Trigger](/docs/api/create-trigger.md)
* [Delete Trigger](/docs/api/delete-trigger.md)

***

## Next Steps[​](#next-steps "Direct link to Next Steps")

* Learn about [Custom Fields](/docs/guides/custom_fields.md) to add webhook triggers on field changes
* Explore [Asset Management](/docs/guides/asset_management.md) to automate workflows
* Read about [Search](/docs/guides/search.md) to find assets programmatically
