Home/Blog/Trello Webhooks: Complete Guide with Payload Examples [2025]
Technology

Trello Webhooks: Complete Guide with Payload Examples [2025]

Complete guide to Trello webhooks for project management automation.

By InventiveHQ Team
Trello Webhooks: Complete Guide with Payload Examples [2025]

When a team member moves a Trello card to "In Progress" or adds a critical comment, you need to know immediately—not when your polling script checks in 5 minutes. Trello webhooks solve this problem by sending real-time HTTP POST notifications to your server the moment board actions occur, enabling you to:

  • Sync project data instantly between Trello and your custom dashboard or CRM
  • Trigger automation workflows when cards move between lists or checklists are completed
  • Track team activity with detailed logs of who changed what and when
  • Send notifications via Slack, email, or SMS when important cards are updated
  • Build custom integrations that extend Trello's functionality for your specific workflow

Trello's webhook system uses a unique HMAC-SHA1 signature approach—instead of signing just the request body, Trello concatenates the body + your callback URL before hashing. This provides an extra layer of security that prevents signature replay attacks even if attackers intercept payloads. In this comprehensive guide, you'll learn how to create Trello webhooks programmatically, implement the unique signature verification in Node.js, Python, and PHP, handle all action types, and build production-ready webhook endpoints.

Testing your Trello integration is crucial before going live. Our Webhook Payload Generator tool lets you create properly signed Trello webhook payloads with custom event data, allowing you to test your signature verification logic (including the unique body + callbackURL concatenation) without creating actual webhooks or exposing your local development environment.

What Are Trello Webhooks?

Trello webhooks are HTTP POST callbacks that Atlassian Trello sends to your specified endpoint URL whenever actions occur on a Trello board, list, card, or member you're monitoring. Unlike traditional API polling where your application repeatedly queries Trello's servers asking "did anything happen?", webhooks invert this model—Trello proactively notifies your server the instant a card is created, moved, commented on, or any other action occurs.

The webhook architecture follows this flow:

[Board Action Occurs] → [Trello Server] → [HTTP POST to Your Endpoint] → [Your Application Logic]

Trello webhooks differ from generic webhooks and other provider implementations in several important ways:

  1. No Dashboard Configuration: Webhooks are created entirely via REST API calls, not through the Trello web interface
  2. Model-Based Subscriptions: Subscribe to an entire board, list, card, or member—you cannot filter to specific event types
  3. Unique Signature Method: Concatenates request body + callback URL before HMAC-SHA1 signing (prevents signature replay)
  4. HEAD Request Verification: Trello sends a HEAD request to your endpoint before creating the webhook to verify it's accessible
  5. No Automatic Retry: Failed deliveries don't trigger retries—webhooks become inactive after repeated failures

Benefits specific to Trello webhooks:

  • Real-time tracking of 40+ action types (card movements, comments, attachments, checklist changes, etc.)
  • Subscribe to entire boards or individual cards for granular control
  • Signature includes callback URL, preventing cross-endpoint signature replay attacks
  • Simple JSON payload structure with consistent action/model format
  • Built-in support for monitoring board changes, member activity, and card updates

Prerequisites for setting up Trello webhooks:

  • Active Trello account with boards you want to monitor
  • Trello API key and app secret from https://trello.com/app-key
  • User token with read permissions for the boards you'll monitor
  • Publicly accessible HTTPS endpoint (localhost won't work without tunneling)
  • SSL/TLS certificate on your webhook endpoint (required for security)
  • Ability to handle HTTP HEAD requests for endpoint verification

Understanding Trello's Unique Signature Approach

Before diving into setup, it's critical to understand how Trello's signature verification differs from other webhook providers.

Standard Webhook Signature (e.g., GitHub, Stripe):

signature = HMAC-SHA256(request_body, secret_key)

Trello's Unique Signature:

signature = HMAC-SHA1(request_body + callback_url, app_secret)

Why This Matters:

The callback URL is part of the signed data, which means:

  1. Different URLs get different signatures - Even if two webhooks receive identical payloads, their signatures will differ if registered to different callback URLs
  2. Signature replay prevention - Attackers cannot capture a webhook from one endpoint and replay it to another
  3. Callback URL validation - You must know and include your registered callback URL when verifying signatures

Example:

Payload: {"action": {"type": "createCard"}}

Callback URL A: https://app.example.com/webhooks/trello Callback URL B: https://other.example.com/webhooks/trello

Even though the payload is identical, the signatures will be completely different because Trello concatenates:

  • URL A: {"action": {"type": "createCard"}}https://app.example.com/webhooks/trello
  • URL B: {"action": {"type": "createCard"}}https://other.example.com/webhooks/trello

This unique approach means you must store your callback URL alongside your app secret to verify signatures correctly.

Setting Up Trello Webhooks

Unlike most webhook providers, Trello webhooks are created programmatically via API calls, not through a web dashboard. Follow these step-by-step instructions.

Step 1: Get Your API Credentials

  1. Navigate to Trello API Key Page

  2. Copy Your API Key

    • Your API key is displayed at the top of the page
    • Copy and store this securely (e.g., YOUR_API_KEY)
  3. Copy Your App Secret

    • Scroll down to find "App Secret" section
    • Click "Show" to reveal your app secret
    • Copy and store this securely (e.g., YOUR_APP_SECRET)
    • This secret is used for webhook signature verification
  4. Generate a User Token

    • Click the "Token" link on the same page
    • Authorize the token with "read" permissions
    • Copy the generated token (e.g., YOUR_TOKEN)
    • This token authenticates API requests to create webhooks

Store these securely in environment variables:

TRELLO_API_KEY=your_api_key_here
TRELLO_APP_SECRET=your_app_secret_here
TRELLO_TOKEN=your_user_token_here

Step 2: Identify the Model ID

Trello webhooks subscribe to a "model" - typically a board ID, but can also be a list, card, or member ID.

Get Board ID:

  1. Open the Trello board you want to monitor
  2. Add .json to the board URL in your browser
    • Example: https://trello.com/b/abc123/my-boardhttps://trello.com/b/abc123/my-board.json
  3. Find the id field in the JSON response
  4. Copy the board ID (e.g., board_jkl012)

Alternative: Use the Trello API

# List all boards for your account
curl "https://api.trello.com/1/members/me/boards?key=YOUR_API_KEY&token=YOUR_TOKEN"

# Find your board's ID in the response

Step 3: Prepare Your Webhook Endpoint

Your endpoint must:

  1. Respond to HEAD requests - Trello verifies endpoint accessibility before creating the webhook
  2. Return 200 status for HEAD requests
  3. Handle POST requests with JSON payloads
  4. Respond quickly (within 30 seconds)

Minimal endpoint example (Node.js/Express):

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

// IMPORTANT: Trello sends HEAD request to verify endpoint
app.head('/webhooks/trello', (req, res) => {
  res.status(200).send();
});

// Handle webhook payloads
app.post('/webhooks/trello', express.json(), (req, res) => {
  console.log('Received Trello webhook:', req.body);
  res.status(200).send();
});

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

Deploy your endpoint or use ngrok for local testing:

# If testing locally with ngrok
ngrok http 3000

# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)

Step 4: Create the Webhook via API

Use the Trello REST API to create your webhook:

cURL Example:

curl -X POST "https://api.trello.com/1/webhooks" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "YOUR_API_KEY",
    "token": "YOUR_TOKEN",
    "description": "Production Board Monitor",
    "callbackURL": "https://yourapp.com/webhooks/trello",
    "idModel": "board_jkl012"
  }'

Node.js Example:

const axios = require('axios');

async function createTrelloWebhook() {
  try {
    const response = await axios.post('https://api.trello.com/1/webhooks', {
      key: process.env.TRELLO_API_KEY,
      token: process.env.TRELLO_TOKEN,
      description: 'Production Board Monitor',
      callbackURL: 'https://yourapp.com/webhooks/trello',
      idModel: 'board_jkl012'
    });

    console.log('Webhook created:', response.data);
    console.log('Webhook ID:', response.data.id);

    // IMPORTANT: Store webhook ID for later management
    return response.data.id;
  } catch (error) {
    console.error('Failed to create webhook:', error.response.data);
  }
}

createTrelloWebhook();

Python Example:

import requests
import os

def create_trello_webhook():
    url = 'https://api.trello.com/1/webhooks'

    payload = {
        'key': os.environ['TRELLO_API_KEY'],
        'token': os.environ['TRELLO_TOKEN'],
        'description': 'Production Board Monitor',
        'callbackURL': 'https://yourapp.com/webhooks/trello',
        'idModel': 'board_jkl012'
    }

    response = requests.post(url, json=payload)

    if response.status_code == 200:
        webhook_data = response.json()
        print(f"Webhook created: {webhook_data}")
        print(f"Webhook ID: {webhook_data['id']}")
        return webhook_data['id']
    else:
        print(f"Failed to create webhook: {response.json()}")

webhook_id = create_trello_webhook()

Important Notes:

  • Trello immediately sends a HEAD request to your callback URL to verify it's accessible
  • If the HEAD request fails, webhook creation will fail with an error
  • The webhook becomes active immediately upon successful creation
  • Store the returned webhook ID for later management (updating, deleting, checking status)

Step 5: Verify Webhook Creation

Check that your webhook was created successfully:

# List all webhooks for your token
curl "https://api.trello.com/1/tokens/YOUR_TOKEN/webhooks?key=YOUR_API_KEY"

# Get specific webhook details
curl "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=YOUR_API_KEY&token=YOUR_TOKEN"

Successful webhook response:

{
  "id": "webhook_abc123",
  "description": "Production Board Monitor",
  "idModel": "board_jkl012",
  "callbackURL": "https://yourapp.com/webhooks/trello",
  "active": true
}

The active: true field indicates the webhook is operational.

Managing Trello Webhooks

Update Webhook:

curl -X PUT "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=API_KEY&token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Updated Description",
    "callbackURL": "https://newurl.com/webhooks/trello"
  }'

Delete Webhook:

curl -X DELETE "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=API_KEY&token=TOKEN"

Check Webhook Status:

curl "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=API_KEY&token=TOKEN"

If active: false, the webhook has been deactivated due to delivery failures and must be recreated.

Trello Webhook Events & Payloads

Trello sends webhook payloads for ALL actions on the subscribed model (board). You cannot filter event types during webhook creation—filter in your application by checking action.type.

Payload Structure Overview

Every Trello webhook payload follows this format:

{
  "action": {
    "id": "action_abc123",
    "type": "createCard",
    "date": "2025-01-24T12:00:00.000Z",
    "memberCreator": {
      "id": "member123",
      "username": "johndoe",
      "fullName": "John Doe",
      "avatarUrl": "https://..."
    },
    "data": {
      // Event-specific data varies by action type
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.id - Unique identifier for this action (use for idempotency)
  • action.type - Event type (createCard, updateCard, commentCard, etc.)
  • action.date - ISO 8601 timestamp when action occurred
  • action.memberCreator - User who triggered the action
  • action.data - Event-specific details (varies by type)
  • model - The board/model the action occurred on

Common Event Types

Action TypeDescriptionTypical Use Case
createCardNew card created on the boardTrack new tasks, trigger notifications
updateCardCard modified (name, description, list, etc.)Monitor card movements, status changes
commentCardComment added to a cardTrack team communication, trigger alerts
addMemberToCardMember assigned to a cardMonitor task assignments, send notifications
updateCheckItemStateOnCardChecklist item completed/uncompletedTrack task progress, trigger workflows
deleteCardCard deleted from boardArchive data, log deletions
addAttachmentToCardFile attached to a cardDownload attachments, sync files
createListNew list created on boardTrack workflow changes
updateListList renamed or closedMonitor board structure changes
moveCardToBoardCard moved to different boardSync cross-board data

Detailed Event Examples

Event: createCard

Description: A new card was created on the board.

Payload Structure:

{
  "action": {
    "id": "action_abc123",
    "type": "createCard",
    "date": "2025-01-24T12:00:00.000Z",
    "memberCreator": {
      "id": "member123",
      "username": "johndoe",
      "fullName": "John Doe",
      "avatarUrl": "https://trello-members.s3.amazonaws.com/avatar.png"
    },
    "data": {
      "card": {
        "id": "card_def456",
        "name": "Implement new feature",
        "desc": "Add user authentication to the dashboard",
        "due": null,
        "idList": "list_ghi789",
        "shortLink": "abc123"
      },
      "board": {
        "id": "board_jkl012",
        "name": "Sprint Planning",
        "shortLink": "def456"
      },
      "list": {
        "id": "list_ghi789",
        "name": "To Do"
      }
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.data.card - Complete card details including name, description, due date
  • action.data.list - The list where the card was created
  • action.data.card.shortLink - Short URL identifier (use for card links)

Use Cases:

  • Send Slack notification when high-priority cards are created
  • Auto-assign cards to team members based on card name keywords
  • Log new tasks in your project management system

Event: updateCard

Description: An existing card was modified (name changed, moved to different list, etc.).

Payload Structure:

{
  "action": {
    "id": "action_xyz789",
    "type": "updateCard",
    "date": "2025-01-24T13:30:00.000Z",
    "memberCreator": {
      "id": "member456",
      "username": "janesmith",
      "fullName": "Jane Smith"
    },
    "data": {
      "old": {
        "idList": "list_ghi789"
      },
      "card": {
        "id": "card_def456",
        "name": "Implement new feature",
        "idList": "list_mno345",
        "shortLink": "abc123"
      },
      "board": {
        "id": "board_jkl012",
        "name": "Sprint Planning"
      },
      "listAfter": {
        "id": "list_mno345",
        "name": "In Progress"
      },
      "listBefore": {
        "id": "list_ghi789",
        "name": "To Do"
      }
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.data.old - Previous values (only includes changed fields)
  • action.data.listBefore - List card was in before update
  • action.data.listAfter - List card moved to (if applicable)

Common Update Types:

  • List changes (idList changed) - Card moved between lists
  • Name/description changes - Card details updated
  • Due date changes - Deadline added/modified
  • Label changes - Card categorization updated

Use Cases:

  • Track workflow progression when cards move to "Done" list
  • Send reminders when due dates are added
  • Log card history for audit trails

Event: commentCard

Description: A comment was added to a card.

Payload Structure:

{
  "action": {
    "id": "action_comment123",
    "type": "commentCard",
    "date": "2025-01-24T14:15:00.000Z",
    "memberCreator": {
      "id": "member789",
      "username": "bobwilson",
      "fullName": "Bob Wilson"
    },
    "data": {
      "text": "Great progress on this task! Let me know if you need any help.",
      "card": {
        "id": "card_def456",
        "name": "Implement new feature",
        "shortLink": "abc123"
      },
      "board": {
        "id": "board_jkl012",
        "name": "Sprint Planning"
      }
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.data.text - Full comment text
  • action.data.card - Card that was commented on

Use Cases:

  • Send email notifications for @mentions in comments
  • Extract action items from comments using AI/NLP
  • Track team collaboration and communication patterns

Event: addMemberToCard

Description: A team member was assigned to a card.

Payload Structure:

{
  "action": {
    "id": "action_member456",
    "type": "addMemberToCard",
    "date": "2025-01-24T15:00:00.000Z",
    "memberCreator": {
      "id": "member123",
      "username": "johndoe",
      "fullName": "John Doe"
    },
    "data": {
      "card": {
        "id": "card_def456",
        "name": "Implement new feature",
        "shortLink": "abc123"
      },
      "board": {
        "id": "board_jkl012",
        "name": "Sprint Planning"
      },
      "member": {
        "id": "member456",
        "username": "janesmith",
        "fullName": "Jane Smith"
      }
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.data.member - The member who was assigned to the card
  • action.memberCreator - The person who made the assignment (may be different)

Use Cases:

  • Notify assigned members via email or Slack
  • Update workload tracking systems
  • Trigger onboarding workflows for new tasks

Event: updateCheckItemStateOnCard

Description: A checklist item was checked or unchecked.

Payload Structure:

{
  "action": {
    "id": "action_check789",
    "type": "updateCheckItemStateOnCard",
    "date": "2025-01-24T16:00:00.000Z",
    "memberCreator": {
      "id": "member456",
      "username": "janesmith",
      "fullName": "Jane Smith"
    },
    "data": {
      "checkItem": {
        "id": "checkitem_abc123",
        "name": "Write unit tests",
        "state": "complete"
      },
      "card": {
        "id": "card_def456",
        "name": "Implement new feature",
        "shortLink": "abc123"
      },
      "board": {
        "id": "board_jkl012",
        "name": "Sprint Planning"
      },
      "checklist": {
        "id": "checklist_xyz789",
        "name": "Development Tasks"
      }
    }
  },
  "model": {
    "id": "board_jkl012",
    "name": "Sprint Planning"
  }
}

Key Fields:

  • action.data.checkItem - The checklist item with its state (complete/incomplete)
  • action.data.checklist - Parent checklist containing the item

Use Cases:

  • Track task completion percentages
  • Trigger workflows when all checklist items are complete
  • Monitor developer progress on subtasks

Webhook Signature Verification

Why Signature Verification Matters

Without signature verification, attackers could:

  • Spoof webhook requests by sending fake action events to your endpoint
  • Manipulate data by injecting false card updates or deletions
  • Trigger unintended actions like creating duplicate tasks or sending false notifications
  • Exploit business logic by fabricating member assignments or checklist completions

Trello's unique HMAC-SHA1 signature (body + callback URL) cryptographically proves that webhook requests genuinely originated from Trello's servers and are intended for your specific endpoint.

Trello's Signature Method

Algorithm: HMAC-SHA1 with base64 encoding

Header Name: X-Trello-Webhook

What's Signed: Trello creates a signature by:

  1. Concatenating the raw request body bytes + callback URL string
  2. Computing HMAC-SHA1 hash using your app secret as the key
  3. Base64-encoding the hash for transmission

Formula:

data_to_sign = request_body + callback_url
signature = base64(HMAC-SHA1(data_to_sign, app_secret))

Example:

Request body: {"action":{"type":"createCard"}} Callback URL: https://yourapp.com/webhooks/trello App secret: your_app_secret_here

Data to sign: {"action":{"type":"createCard"}}https://yourapp.com/webhooks/trello
HMAC-SHA1 hash: (binary hash)
Base64 encoded: dGVzdHNpZ25hdHVyZQ== (sent in X-Trello-Webhook header)

Step-by-Step Verification Process

  1. Extract the signature from the X-Trello-Webhook header
  2. Get the raw request body (must be unmodified bytes, not parsed JSON)
  3. Retrieve your callback URL from configuration (the URL you registered the webhook with)
  4. Concatenate body + callback URL in exact order (body first, then URL)
  5. Compute HMAC-SHA1 using your app secret as the key
  6. Base64 encode the computed hash
  7. Compare signatures using constant-time comparison to prevent timing attacks

Code Examples

Node.js / Express

const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');

const app = express();

const APP_SECRET = process.env.TRELLO_APP_SECRET;
const CALLBACK_URL = process.env.TRELLO_CALLBACK_URL; // https://yourapp.com/webhooks/trello

// Verification function
const verifyTrelloSignature = (body, callbackUrl, signature, secret) => {
  // Concatenate body + callback URL
  const dataToSign = body + callbackUrl;

  // Compute HMAC-SHA1
  const expectedSignature = crypto
    .createHmac('sha1', secret)
    .update(dataToSign, 'utf8')
    .digest('base64');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
};

// HEAD request handler (required for webhook creation)
app.head('/webhooks/trello', (req, res) => {
  res.status(200).send();
});

// Webhook endpoint with raw body parsing
app.post('/webhooks/trello',
  bodyParser.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      // Extract signature from header
      const signature = req.get('X-Trello-Webhook');

      if (!signature) {
        console.error('Missing Trello webhook signature');
        return res.status(403).json({ error: 'Missing signature' });
      }

      // Get raw body as string
      const rawBody = req.body.toString('utf8');

      // Verify signature
      const isValid = verifyTrelloSignature(
        rawBody,
        CALLBACK_URL,
        signature,
        APP_SECRET
      );

      if (!isValid) {
        console.error('Invalid Trello webhook signature');
        return res.status(403).json({ error: 'Invalid signature' });
      }

      // Parse payload after verification
      const payload = JSON.parse(rawBody);

      console.log(`Trello webhook: ${payload.action.type} on card ${payload.action.data.card?.name || 'unknown'}`);

      // Return 200 immediately
      res.status(200).send();

      // Process async (see Implementation Example section)
      processWebhookAsync(payload);

    } catch (error) {
      console.error('Trello webhook processing error:', error);
      res.status(500).json({ error: 'Processing failed' });
    }
  }
);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Trello webhook server listening on port ${PORT}`);
});

Python / Flask

import hmac
import hashlib
import base64
from flask import Flask, request, jsonify

app = Flask(__name__)

APP_SECRET = 'your_app_secret_here'
CALLBACK_URL = 'https://yourapp.com/webhooks/trello'

def verify_trello_signature(body, callback_url, signature, secret):
    """Verify Trello webhook signature"""
    # Concatenate body + callback URL
    data_to_sign = body + callback_url

    # Compute HMAC-SHA1
    expected_signature = base64.b64encode(
        hmac.new(
            secret.encode('utf-8'),
            data_to_sign.encode('utf-8'),
            hashlib.sha1
        ).digest()
    ).decode('utf-8')

    # Constant-time comparison
    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/trello', methods=['HEAD'])
def trello_webhook_head():
    """Handle HEAD request for webhook verification"""
    return '', 200

@app.route('/webhooks/trello', methods=['POST'])
def trello_webhook():
    try:
        # Extract signature from header
        signature = request.headers.get('X-Trello-Webhook')

        if not signature:
            print('Missing Trello webhook signature')
            return jsonify({'error': 'Missing signature'}), 403

        # Get raw body
        raw_body = request.get_data(as_text=True)

        # Verify signature
        is_valid = verify_trello_signature(
            raw_body,
            CALLBACK_URL,
            signature,
            APP_SECRET
        )

        if not is_valid:
            print('Invalid Trello webhook signature')
            return jsonify({'error': 'Invalid signature'}), 403

        # Parse payload after verification
        payload = request.get_json()

        action_type = payload['action']['type']
        card_name = payload['action']['data'].get('card', {}).get('name', 'unknown')

        print(f'Trello webhook: {action_type} on card {card_name}')

        # Return 200 immediately
        return '', 200

    except Exception as error:
        print(f'Trello webhook processing error: {error}')
        return jsonify({'error': 'Processing failed'}), 500

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

PHP

<?php
// Get app secret and callback URL from environment
$appSecret = getenv('TRELLO_APP_SECRET');
$callbackUrl = getenv('TRELLO_CALLBACK_URL'); // https://yourapp.com/webhooks/trello

// Handle HEAD request for webhook verification
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') {
    http_response_code(200);
    exit;
}

// Extract signature from header
$signature = $_SERVER['HTTP_X_TRELLO_WEBHOOK'] ?? '';

if (empty($signature)) {
    error_log('Missing Trello webhook signature');
    http_response_code(403);
    die(json_encode(['error' => 'Missing signature']));
}

// Get raw POST body (MUST be raw bytes, not parsed)
$rawBody = file_get_contents('php://input');

// Concatenate body + callback URL
$dataToSign = $rawBody . $callbackUrl;

// Compute HMAC-SHA1
$expectedSignature = base64_encode(
    hash_hmac('sha1', $dataToSign, $appSecret, true)
);

// Constant-time comparison
if (!hash_equals($signature, $expectedSignature)) {
    error_log('Invalid Trello webhook signature');
    http_response_code(403);
    die(json_encode(['error' => 'Invalid signature']));
}

// Parse payload after verification
$payload = json_decode($rawBody, true);

$actionType = $payload['action']['type'];
$cardName = $payload['action']['data']['card']['name'] ?? 'unknown';

error_log("Trello webhook: {$actionType} on card {$cardName}");

// Return 200 immediately
http_response_code(200);

// Process async (implement your queue logic here)
?>

Common Verification Errors

  • Forgetting callback URL: Signature won't match without including it
    • ✅ Concatenate body + callback URL before hashing
  • Parsing JSON before verification: Body modified, signature breaks
    • ✅ Use raw body parser, verify first, then parse JSON
  • Wrong callback URL: Using different URL than webhook was registered with
    • ✅ Store callback URL used during webhook creation
  • Wrong algorithm: Using SHA-256 instead of SHA-1
    • ✅ Trello uses HMAC-SHA1, not SHA-256
  • Comparing signatures with ==: Vulnerable to timing attacks
    • ✅ Use constant-time comparison functions
  • Encoding mismatches: Incorrect base64 encoding/decoding
    • ✅ Ensure base64 encoding matches Trello's format

Testing Trello Webhooks

Local Development Challenges

Testing Trello webhooks during development presents several obstacles:

  • Trello can't reach localhost: Webhook URLs must be publicly accessible
  • HTTPS requirement: Trello only sends to secure HTTPS endpoints
  • HEAD request verification: Endpoints must respond to HEAD before webhook creation
  • Signature generation complexity: Manually creating valid signatures with body + callbackURL concatenation is error-prone

Solution 1: ngrok for Local Testing

ngrok creates a secure tunnel from a public URL to your local development server.

Setup Steps:

# Install ngrok (macOS)
brew install ngrok

# Or download from ngrok.com for other platforms

# Start your local webhook server on port 3000
node server.js

# In another terminal, create ngrok tunnel
ngrok http 3000

# ngrok output will show:
# Forwarding https://abc123def456.ngrok.io -> http://localhost:3000

Create Webhook with ngrok URL:

curl -X POST "https://api.trello.com/1/webhooks" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "YOUR_API_KEY",
    "token": "YOUR_TOKEN",
    "description": "Dev Testing",
    "callbackURL": "https://abc123def456.ngrok.io/webhooks/trello",
    "idModel": "YOUR_BOARD_ID"
  }'

Trigger Test Events:

  1. Open your Trello board in the browser
  2. Create a card, move it, add a comment, etc.
  3. Your local server will receive webhook POSTs immediately
  4. View all HTTP traffic in ngrok web interface at http://127.0.0.1:4040

ngrok Pro Tips:

  • Use ngrok http 3000 --log=stdout to see all HTTP traffic in terminal
  • ngrok URLs change on restart (use paid plan for static URLs)
  • Set environment variable for callback URL to avoid hardcoding

Solution 2: Webhook Payload Generator Tool

For testing without creating real webhooks or exposing your local environment, use our specialized tool:

Visit Webhook Payload Generator

Testing Workflow:

  1. Select Provider: Choose "Trello" from the dropdown
  2. Choose Event Type: Select action (createCard, updateCard, commentCard, etc.)
  3. Customize Payload: Edit card names, board IDs, member info
  4. Enter Callback URL: Set your registered webhook callback URL
  5. Generate Signature: Tool creates valid HMAC-SHA1 signature (body + callbackURL)
  6. Copy & Send: Use curl or Postman to POST to http://localhost:3000/webhooks/trello

Example cURL Command:

curl -X POST http://localhost:3000/webhooks/trello \
  -H "Content-Type: application/json" \
  -H "X-Trello-Webhook: <generated-signature>" \
  -d '{"action":{"id":"action_abc123","type":"createCard","date":"2025-01-24T12:00:00.000Z","memberCreator":{"id":"member123","username":"johndoe","fullName":"John Doe"},"data":{"card":{"id":"card_def456","name":"Test Card","shortLink":"abc123"},"board":{"id":"board_jkl012","name":"Test Board"}}},"model":{"id":"board_jkl012","name":"Test Board"}}'

Benefits of Webhook Payload Generator:

  • ✅ Test unique body + callbackURL signature logic
  • ✅ No Trello API access needed
  • ✅ Customize payloads for edge cases
  • ✅ Test error handling with malformed data
  • ✅ No webhook creation/deletion overhead
  • ✅ Instant testing without waiting for real Trello events

Trello's Webhook Verification

When you create a webhook, Trello sends a HEAD request to verify your endpoint:

HEAD /webhooks/trello HTTP/1.1
Host: yourapp.com

Your endpoint must return 200 status or webhook creation will fail.

Test HEAD response:

curl -I -X HEAD https://yourapp.com/webhooks/trello

# Should return:
# HTTP/1.1 200 OK

Testing Checklist

Before deploying to production, verify:

  • HEAD request returns 200 (test with curl -I -X HEAD)
  • Signature verification passes with valid Trello signatures (including callbackURL)
  • Endpoint returns 200 for POST requests within 30 seconds
  • Callback URL stored correctly in environment/config
  • All action types handled (or safely ignored)
  • Idempotent processing handles duplicate action IDs gracefully
  • Error handling catches malformed payloads without crashing
  • Async processing queues events instead of blocking response
  • Logging captures action details for debugging
  • Monitoring alerts on signature verification failures

Test Each Action Type:

Use the Webhook Payload Generator to send:

  • createCard event
  • updateCard event (list change)
  • commentCard event
  • addMemberToCard event
  • updateCheckItemStateOnCard event
  • deleteCard event

Verify your application responds correctly to each scenario.

Implementation Example

Building a production-ready Trello webhook endpoint requires proper error handling, async processing, idempotency checks, and monitoring.

Requirements for Production Endpoints

  • Respond to HEAD requests: Required for webhook creation verification
  • Respond within 30 seconds: Trello doesn't have explicit timeout, but be fast
  • Return 200 status codes: Indicates successful receipt
  • Process asynchronously: Queue events for background processing to respond quickly
  • Implement idempotency: Use action.id to prevent duplicate processing
  • Handle all action types: Filter or process based on action.type
  • Log comprehensively: Track all events for debugging and reconciliation

Complete Node.js Production Example

This example uses Express, Bull queue for async processing, and Prisma for database operations.

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const Queue = require('bull');
const { PrismaClient } = require('@prisma/client');

const app = express();
const prisma = new PrismaClient();

// Configuration
const APP_SECRET = process.env.TRELLO_APP_SECRET;
const CALLBACK_URL = process.env.TRELLO_CALLBACK_URL;

// Create Bull queue for async webhook processing
const webhookQueue = new Queue('trello-webhooks', {
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: process.env.REDIS_PORT || 6379
  }
});

// Signature verification helper
const verifyTrelloSignature = (body, callbackUrl, signature, secret) => {
  const dataToSign = body + callbackUrl;
  const expectedSignature = crypto
    .createHmac('sha1', secret)
    .update(dataToSign, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
};

// HEAD request handler (required for webhook creation)
app.head('/webhooks/trello', (req, res) => {
  console.log('[Trello] HEAD request received');
  res.status(200).send();
});

// Trello webhook endpoint
app.post('/webhooks/trello',
  bodyParser.raw({ type: 'application/json' }),
  async (req, res) => {
    const startTime = Date.now();

    try {
      // 1. Extract signature
      const signature = req.get('X-Trello-Webhook');

      if (!signature) {
        console.error('[Trello] Missing signature header');
        return res.status(403).json({ error: 'Missing signature' });
      }

      // 2. Get raw body
      const rawBody = req.body.toString('utf8');

      // 3. Verify signature
      const isValid = verifyTrelloSignature(
        rawBody,
        CALLBACK_URL,
        signature,
        APP_SECRET
      );

      if (!isValid) {
        console.error('[Trello] Invalid signature');
        return res.status(403).json({ error: 'Invalid signature' });
      }

      // 4. Parse payload
      const payload = JSON.parse(rawBody);
      const actionId = payload.action.id;
      const actionType = payload.action.type;

      console.log(`[Trello] Received ${actionType} action ${actionId}`);

      // 5. Check if already processed (idempotency)
      const exists = await prisma.trelloWebhookEvent.findUnique({
        where: { actionId }
      });

      if (exists) {
        console.log(`[Trello] Action ${actionId} already processed, skipping`);
        return res.status(200).send(); // Still return 200 to avoid retries
      }

      // 6. Queue for async processing
      await webhookQueue.add({
        actionId,
        actionType,
        actionDate: payload.action.date,
        rawPayload: payload
      }, {
        attempts: 3,
        backoff: {
          type: 'exponential',
          delay: 2000
        }
      });

      // 7. Return 200 immediately
      const processingTime = Date.now() - startTime;
      console.log(`[Trello] Queued in ${processingTime}ms`);
      res.status(200).send();

    } catch (error) {
      console.error('[Trello] Webhook processing error:', error);
      // Return 500 to indicate processing failure
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

// Process webhooks from queue
webhookQueue.process(async (job) => {
  const { actionId, actionType, actionDate, rawPayload } = job.data;

  try {
    console.log(`[Queue] Processing ${actionType} action ${actionId}`);

    // Mark as processing in database
    await prisma.trelloWebhookEvent.create({
      data: {
        actionId,
        actionType,
        actionDate: new Date(actionDate),
        status: 'processing',
        rawPayload: JSON.stringify(rawPayload)
      }
    });

    // Handle different action types
    switch (actionType) {
      case 'createCard':
        await handleCreateCard(rawPayload);
        break;

      case 'updateCard':
        await handleUpdateCard(rawPayload);
        break;

      case 'commentCard':
        await handleCommentCard(rawPayload);
        break;

      case 'addMemberToCard':
        await handleAddMemberToCard(rawPayload);
        break;

      case 'updateCheckItemStateOnCard':
        await handleCheckItemUpdate(rawPayload);
        break;

      case 'deleteCard':
        await handleDeleteCard(rawPayload);
        break;

      default:
        console.log(`[Queue] Unhandled action type: ${actionType}`);
    }

    // Mark as completed
    await prisma.trelloWebhookEvent.update({
      where: { actionId },
      data: {
        status: 'completed',
        processedAt: new Date()
      }
    });

    console.log(`[Queue] Completed ${actionType} action ${actionId}`);

  } catch (error) {
    console.error(`[Queue] Failed to process action ${actionId}:`, error);

    // Mark as failed
    await prisma.trelloWebhookEvent.update({
      where: { actionId },
      data: {
        status: 'failed',
        errorMessage: error.message,
        failedAt: new Date()
      }
    });

    throw error; // Trigger Bull queue retry
  }
});

// Business logic handlers

async function handleCreateCard(payload) {
  const card = payload.action.data.card;
  const list = payload.action.data.list;
  const creator = payload.action.memberCreator;

  // Store card in your database
  await prisma.card.create({
    data: {
      trelloCardId: card.id,
      name: card.name,
      description: card.desc || '',
      listId: list.id,
      listName: list.name,
      createdBy: creator.fullName,
      createdAt: new Date(payload.action.date)
    }
  });

  console.log(`[Handler] Created card: ${card.name} in ${list.name}`);

  // Trigger notifications
  if (card.name.includes('[URGENT]')) {
    await sendUrgentCardNotification(card, creator);
  }
}

async function handleUpdateCard(payload) {
  const card = payload.action.data.card;
  const oldValues = payload.action.data.old;
  const listAfter = payload.action.data.listAfter;
  const listBefore = payload.action.data.listBefore;

  // Check if card moved to different list
  if (oldValues.idList && listAfter) {
    console.log(`[Handler] Card ${card.name} moved: ${listBefore.name} → ${listAfter.name}`);

    // Update card list in database
    await prisma.card.updateMany({
      where: { trelloCardId: card.id },
      data: {
        listId: listAfter.id,
        listName: listAfter.name,
        lastMovedAt: new Date(payload.action.date)
      }
    });

    // Trigger workflow if moved to "Done"
    if (listAfter.name === 'Done') {
      await handleCardCompleted(card);
    }
  }
}

async function handleCommentCard(payload) {
  const comment = payload.action.data.text;
  const card = payload.action.data.card;
  const commenter = payload.action.memberCreator;

  // Store comment
  await prisma.comment.create({
    data: {
      cardId: card.id,
      text: comment,
      author: commenter.fullName,
      createdAt: new Date(payload.action.date)
    }
  });

  console.log(`[Handler] Comment on ${card.name}: ${comment}`);

  // Check for @mentions
  if (comment.includes('@')) {
    await handleMentions(comment, card);
  }
}

async function handleAddMemberToCard(payload) {
  const card = payload.action.data.card;
  const member = payload.action.data.member;
  const assigner = payload.action.memberCreator;

  console.log(`[Handler] ${member.fullName} assigned to ${card.name} by ${assigner.fullName}`);

  // Notify assigned member
  await sendAssignmentNotification(member, card, assigner);
}

async function handleCheckItemUpdate(payload) {
  const checkItem = payload.action.data.checkItem;
  const card = payload.action.data.card;

  console.log(`[Handler] Checklist item "${checkItem.name}" marked as ${checkItem.state} on ${card.name}`);

  // Update checklist progress
  await updateChecklistProgress(card.id);
}

async function handleDeleteCard(payload) {
  const card = payload.action.data.card;

  console.log(`[Handler] Card deleted: ${card.name}`);

  // Archive or soft-delete in database
  await prisma.card.updateMany({
    where: { trelloCardId: card.id },
    data: {
      deletedAt: new Date(payload.action.date)
    }
  });
}

// Helper functions (implement based on your needs)

async function sendUrgentCardNotification(card, creator) {
  console.log(`[Notification] URGENT card created: ${card.name} by ${creator.fullName}`);
  // Send Slack/email notification
}

async function handleCardCompleted(card) {
  console.log(`[Workflow] Card completed: ${card.name}`);
  // Trigger completion workflow
}

async function handleMentions(comment, card) {
  const mentions = comment.match(/@\w+/g);
  console.log(`[Mentions] Found mentions in comment on ${card.name}: ${mentions}`);
  // Send notifications to mentioned users
}

async function sendAssignmentNotification(member, card, assigner) {
  console.log(`[Notification] Notifying ${member.fullName} of assignment to ${card.name}`);
  // Send email/Slack notification
}

async function updateChecklistProgress(cardId) {
  console.log(`[Progress] Updating checklist progress for card ${cardId}`);
  // Calculate and store checklist completion percentage
}

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString()
  });
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, closing server gracefully');
  await webhookQueue.close();
  await prisma.$disconnect();
  process.exit(0);
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Trello webhook server listening on port ${PORT}`);
  console.log(`Endpoint: POST /webhooks/trello`);
  console.log(`Callback URL: ${CALLBACK_URL}`);
});

Database Schema Example (Prisma)

model TrelloWebhookEvent {
  id            String   @id @default(cuid())
  actionId      String   @unique // Trello action.id
  actionType    String   // createCard, updateCard, etc.
  actionDate    DateTime
  status        String   // processing, completed, failed
  rawPayload    String   @db.Text
  errorMessage  String?
  processedAt   DateTime?
  failedAt      DateTime?
  createdAt     DateTime @default(now())

  @@index([actionType, createdAt])
}

model Card {
  id           String    @id @default(cuid())
  trelloCardId String    @unique
  name         String
  description  String    @db.Text
  listId       String
  listName     String
  createdBy    String
  createdAt    DateTime
  lastMovedAt  DateTime?
  deletedAt    DateTime?

  @@index([listId, deletedAt])
}

model Comment {
  id        String   @id @default(cuid())
  cardId    String
  text      String   @db.Text
  author    String
  createdAt DateTime

  @@index([cardId, createdAt])
}

Best Practices

Security

  • Always verify signatures: Include callback URL in verification
  • Store secrets securely: Environment variables, never commit to git
  • Use HTTPS only: Trello requires HTTPS endpoints
  • Validate callback URL: Store and verify it matches webhook registration
  • Implement rate limiting: Protect against abuse
  • Monitor signature failures: Alert on sudden increase in invalid signatures
  • Rotate secrets periodically: Update app secrets and recreate webhooks
  • Isolate webhook endpoints: Don't expose webhook paths in public documentation

Performance

  • Respond within 30 seconds: Use async processing for anything longer
  • Return 200 immediately: Acknowledge receipt, process later
  • Use queue systems: Redis/Bull, RabbitMQ, AWS SQS for async processing
  • Implement connection pooling: Database connections should be pooled
  • Monitor processing times: Alert if queue depth grows
  • Handle HEAD requests efficiently: Simple 200 response, no processing

Reliability

  • Implement idempotency: Store action.id before processing to detect duplicates
  • Handle all action types: Filter or process based on action.type
  • Log all events: Comprehensive logging enables debugging
  • Monitor webhook status: Check active field, recreate if inactive
  • Test failure scenarios: Simulate errors, timeouts, malformed payloads
  • Implement dead letter queues: Store failed events for manual review
  • Alert on webhook inactivity: Monitor for missing webhooks

Monitoring

  • Track webhook delivery rate: Alert if no webhooks received for X minutes
  • Monitor signature verification: Alert on failures
  • Log unique action IDs: Use action.id for tracing events
  • Track action type distribution: Sudden changes warrant investigation
  • Monitor queue depth: Growing queue indicates bottleneck
  • Set up health checks: Ensure endpoint is reachable and responsive

Trello-Specific Best Practices

Webhook Lifecycle Management:

  • Trello webhooks can become inactive after repeated delivery failures
  • Monitor the active field by querying webhook details periodically
  • Automatically recreate webhooks when they become inactive
  • Store webhook IDs in your database for management

Action Filtering:

  • Trello sends ALL actions for the subscribed model (board)
  • Filter unwanted action types in your application, not at webhook creation
  • Common filters: ignore updateBoard, addLabelToCard unless needed
  • Use switch/case statements for action type routing

Callback URL Management:

  • Store callback URL in environment variables alongside app secret
  • Use the same callback URL for verification that was used during webhook creation
  • Document callback URLs for each environment (dev, staging, production)
  • Update webhooks if callback URL changes (delete old, create new)

Rate Limits:

  • Trello doesn't publish explicit webhook rate limits
  • API rate limit: 300 requests per 10 seconds per API key
  • Webhook creation counts against API rate limit
  • Use exponential backoff when recreating webhooks

Common Issues & Troubleshooting

Issue 1: Signature Verification Failing

Symptoms:

  • 403 errors in your application logs
  • "Invalid signature" messages
  • Webhooks not processing despite Trello sending them

Causes & Solutions:

Callback URL mismatch: Using different URL than webhook was registered with

Solution: Verify your CALLBACK_URL environment variable matches exactly what you used during webhook creation (including trailing slashes, http/https, etc.)

# Check your webhook details
curl "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=API_KEY&token=TOKEN"

# Verify callbackURL field matches your environment variable

Parsing JSON before verification: Body modified, signature won't match

Solution: Use raw body parser for webhook endpoint, verify signature before calling JSON.parse()

// Wrong: JSON parsed before verification
app.use(express.json()); // Applied to ALL routes
app.post('/webhooks/trello', (req, res) => {
  const signature = req.get('X-Trello-Webhook');
  const body = JSON.stringify(req.body); // Body already parsed!
  // Signature verification will fail
});

// Correct: Raw body for webhook endpoint only
app.post('/webhooks/trello', express.raw({type: 'application/json'}), (req, res) => {
  const rawBody = req.body.toString('utf8'); // Raw string
  // Verify signature with raw bytes
});

Using SHA-256 instead of SHA-1: Wrong algorithm

Solution: Trello uses HMAC-SHA1, not HMAC-SHA256

// Wrong algorithm
const hash = crypto.createHmac('sha256', secret); // ❌

// Correct algorithm
const hash = crypto.createHmac('sha1', secret); // ✅

Issue 2: Webhook Creation Failing

Symptoms:

  • API returns error when creating webhook
  • "Webhook callback URL is unreachable" error
  • Webhook creation succeeds but never sends events

Causes & Solutions:

Endpoint not responding to HEAD requests: Trello verifies before creating

Solution: Implement HEAD request handler that returns 200

app.head('/webhooks/trello', (req, res) => {
  res.status(200).send();
});

Endpoint not publicly accessible: Using localhost or firewall blocking

Solution: Use ngrok for local testing or deploy to publicly accessible server

# Test endpoint accessibility
curl -I -X HEAD https://yourapp.com/webhooks/trello

# Should return HTTP/1.1 200 OK

SSL certificate issues: Expired or invalid certificate

Solution: Verify SSL certificate with:

curl -vI https://yourapp.com/webhooks/trello

# Check for SSL errors in output

Issue 3: Webhook Becomes Inactive

Symptoms:

  • Webhook stops receiving events
  • active: false in webhook details
  • No errors in application logs

Cause:

Trello deactivates webhooks after consecutive delivery failures (endpoint returning errors or timing out).

Solution:

Monitor webhook status and recreate when inactive:

async function checkWebhookHealth(webhookId) {
  const response = await axios.get(
    `https://api.trello.com/1/webhooks/${webhookId}`,
    {
      params: {
        key: process.env.TRELLO_API_KEY,
        token: process.env.TRELLO_TOKEN
      }
    }
  );

  if (!response.data.active) {
    console.error('Webhook inactive, recreating...');
    await recreateWebhook(webhookId);
  }
}

// Run health check periodically (e.g., every hour)
setInterval(() => checkWebhookHealth('YOUR_WEBHOOK_ID'), 3600000);

Issue 4: Missing Webhooks

Symptoms:

  • Expected webhooks not arriving
  • Gaps in event data despite Trello actions occurring
  • Webhook shows active: true but no POSTs received

Causes & Solutions:

Firewall blocking: Corporate firewall blocking inbound HTTPS

Solution: Check firewall rules, allowlist Trello IPs (Trello doesn't publish IP ranges, monitor logs for source IPs)


Wrong model ID: Webhook subscribed to different board

Solution: Verify idModel in webhook details matches your intended board ID

curl "https://api.trello.com/1/webhooks/WEBHOOK_ID?key=API_KEY&token=TOKEN"

# Check idModel field

Debugging Checklist

  • Verify webhook exists (GET /1/webhooks/{id})
  • Check webhook is active (active: true in webhook details)
  • Test HEAD request (curl -I -X HEAD your-endpoint)
  • Verify signature with Webhook Payload Generator tool
  • Check application logs for errors during processing
  • Confirm SSL certificate is valid (not expired)
  • Test callback URL accessibility from external server (not your network)
  • Verify model ID is correct (board you're monitoring)
  • Check rate limiting isn't blocking requests
  • Monitor queue depth if using async processing

Frequently Asked Questions

Q: Can I subscribe to specific event types only?

A: No, Trello webhooks send ALL actions for the subscribed model (board). You must filter event types in your application by checking action.type in the payload and ignoring unwanted events.

Q: What happens if my endpoint is down?

A: Unlike some webhook providers, Trello does not implement automatic retry logic. After multiple consecutive delivery failures, Trello will mark the webhook as active: false. You must monitor webhook health and recreate webhooks programmatically when they become inactive.

Q: Can I use the same webhook for multiple boards?

A: No, each webhook subscribes to a single idModel (typically a board ID). To monitor multiple boards, create separate webhooks for each board, each with its own webhook ID.

Q: How long do Trello webhooks stay active?

A: Webhooks remain active indefinitely unless they fail repeatedly (causing deactivation) or the associated model (board) is deleted. Monitor the active field and recreate webhooks as needed.

Q: Why is the signature verification unique?

A: Trello concatenates the request body + callback URL before signing. This prevents signature replay attacks—even if an attacker captures a webhook payload and signature, they cannot replay it to a different endpoint because the callback URL is part of the signed data.

Q: How do I test webhooks without ngrok?

A: Use our Webhook Payload Generator to create properly signed test payloads with the unique body + callbackURL concatenation. This lets you test signature verification logic without exposing your local environment or creating real Trello webhooks.

Q: What's the difference between API key and app secret?

A: The API key is a public identifier used when making API requests to Trello. The app secret is a private key used to verify webhook signatures. Both are obtained from https://trello.com/app-key. Never share your app secret publicly.

Q: Can I filter which boards a webhook monitors?

A: Webhooks subscribe to a single model ID (typically a board). To monitor specific boards only, create separate webhooks with different idModel values for each board you want to track.

Q: Why do I need to handle HEAD requests?

A: Trello sends a HEAD request to your callback URL before creating the webhook to verify the endpoint is accessible. If your endpoint doesn't respond with 200 to HEAD requests, webhook creation will fail.

Next Steps & Resources

Try It Yourself

Ready to implement Trello webhooks? Follow these steps:

  1. Get credentials from https://trello.com/app-key (API key, app secret, token)
  2. Test with our tool: Visit the Webhook Payload Generator to create signed test payloads
  3. Implement signature verification using code examples from this guide (Node.js, Python, or PHP)
  4. Create webhook via API with your board ID and callback URL
  5. Deploy to production with async processing, idempotency checks, and monitoring

Additional Resources

Trello Official Documentation:

Related Guides on InventiveHQ:

Testing & Development Tools:

  • Webhook Payload Generator - Create signed test payloads for Trello (includes unique body + callbackURL)
  • ngrok - Expose localhost for webhook testing

Need Help?

Conclusion

Trello webhooks provide a powerful real-time notification system that enables seamless integration between Trello boards and your custom applications. By following this guide, you now know how to:

  • ✅ Create Trello webhooks programmatically via the REST API
  • ✅ Verify webhook signatures securely using the unique body + callbackURL concatenation with HMAC-SHA1
  • ✅ Implement production-ready webhook endpoints with async processing
  • ✅ Handle all action types from card creation to checklist updates
  • ✅ Test webhooks effectively using ngrok or our payload generator tool
  • ✅ Troubleshoot common issues like signature failures and inactive webhooks

Remember these key principles:

  1. Include callback URL in signature verification - Trello's unique approach prevents replay attacks
  2. Respond to HEAD requests - Required for webhook creation verification
  3. Process asynchronously - Queue events for background processing for reliability at scale
  4. Implement idempotency - Use action.id to detect and handle duplicate events gracefully
  5. Monitor webhook health - Recreate webhooks when they become inactive

Trello's webhook system combines simplicity (straightforward JSON payloads), security (unique signature method with callback URL), and flexibility (40+ action types) to give you complete visibility into board activity. Whether you're syncing Trello with your CRM, automating workflows when cards move, or building custom analytics dashboards, webhooks provide the real-time data pipeline your application needs.

Start building with Trello webhooks today, and use our Webhook Payload Generator to test your integration without creating real webhooks or exposing your development environment.

Have questions or run into issues? Visit the Atlassian Community Forums or contact our team for webhook integration assistance.


Sources:

Let's turn this knowledge into action

Get a free 30-minute consultation with our experts. We'll help you apply these insights to your specific situation.