All JourneyBee webhooks use JWT (JSON Web Token) authentication. Each request includes a signed JWT token in the Authorization header that you must verify using your integration UUID as the secret.

JWT Token Structure

JWT tokens contain the following claims:
  • company_uuid - Company that triggered the event
  • user_uuid - User who performed the action
  • event_id - Event type identifier
  • api_key - Your integration’s API key
  • external_settings - Integration configuration

JWT Token Structure

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "lead": {
    "uuid": "lead_12345678-1234-5678-9012-123456789abc",
    "company_name": "Acme Corp",
    "email": "contact@acme.com"
  },
  "configuration": [
    {
      "id": "lead_integration_settings",
      "selected": [
        {
          "id": "unique_field_lead",
          "value": "email"
        }
      ]
    }
  ]
}

JWT Payload

When decoded, the JWT token contains:
{
  "company_uuid": "company_12345678-1234-5678-9012-123456789abc",
  "user_uuid": "user_87654321-4321-8765-2109-876543210987",
  "event_id": "lead_created",
  "api_key": "jb_api_1234567890abcdef",
  "external_settings": {
    "authorisation": [
      {
        "id": "api_key",
        "value": "your_external_api_key"
      }
    ]
  },
  "iat": 1716214515,
  "exp": 1716218115
}
company_uuid
string
required
UUID of the company that triggered the webhook event
user_uuid
string
required
UUID of the user who performed the action
event_id
string
required
Internal event identifier (e.g., lead_created, deal_updated)
api_key
string
Your integration’s API key for additional verification
external_settings
object
Configuration data including authorization settings

Implementation Examples

Node.js/Express with JWT Verification

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

app.use(express.json());

app.post('/webhook', (req, res) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).send('Missing or invalid authorization header');
  }

  const token = authHeader.split('Bearer ')[1];

  try {
    // Verify JWT token using your integration UUID as the secret
    const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);

    console.log('Webhook context:', {
      company: decoded.company_uuid,
      user: decoded.user_uuid,
      event: decoded.event_id,
      apiKey: decoded.api_key
    });

    // Process webhook payload
    const { lead, configuration } = req.body;

    if (lead) {
      console.log(`Processing lead: ${lead.company_name}`);
      await processLead(lead, configuration, decoded);
    }

    res.status(200).send('OK');

  } catch (error) {
    console.error('JWT verification failed:', error.message);
    return res.status(401).send('Invalid token');
  }
});

async function processLead(lead, configuration, context) {
  // Use context for company-specific processing
  console.log(`Processing lead for company: ${context.company_uuid}`);

  // Access configuration settings
  const settings = configuration.find(c => c.id === 'lead_integration_settings');
  const uniqueField = settings?.selected?.find(s => s.id === 'unique_field_lead');

  console.log(`Unique field mapping: ${uniqueField?.value}`);

  // Your integration logic here
}

app.listen(3000);

Python/Flask with JWT Verification

from flask import Flask, request, abort, jsonify
import jwt
import os
import json

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    auth_header = request.headers.get('Authorization')

    if not auth_header or not auth_header.startswith('Bearer '):
        abort(401, 'Missing or invalid authorization header')

    token = auth_header.split('Bearer ')[1]

    try:
        # Verify JWT token using your integration UUID as the secret
        decoded = jwt.decode(token, os.getenv('INTEGRATION_UUID'), algorithms=['HS256'])

        print('Webhook context:', {
            'company': decoded['company_uuid'],
            'user': decoded['user_uuid'],
            'event': decoded['event_id'],
            'api_key': decoded.get('api_key')
        })

        # Process webhook payload
        payload = request.json

        if 'lead' in payload:
            print(f"Processing lead: {payload['lead']['company_name']}")
            process_lead(payload['lead'], payload['configuration'], decoded)
        elif 'deal' in payload:
            print(f"Processing deal: {payload['deal']['label']}")
            process_deal(payload['deal'], payload['configuration'], decoded)
        elif 'partnership' in payload:
            print(f"Processing partnership: {payload['partnership']['company_name']}")
            process_partnership(payload['partnership'], payload['configuration'], decoded)

        return 'OK', 200

    except jwt.InvalidTokenError as e:
        print(f'JWT verification failed: {e}')
        abort(401, 'Invalid token')

def process_lead(lead, configuration, context):
    # Use context for company-specific processing
    print(f"Processing lead for company: {context['company_uuid']}")

    # Access configuration settings
    settings = next((c for c in configuration if c['id'] == 'lead_integration_settings'), None)
    if settings:
        unique_field = next((s for s in settings['selected'] if s['id'] == 'unique_field_lead'), None)
        if unique_field:
            print(f"Unique field mapping: {unique_field['value']}")

    # Your integration logic here
    pass

def process_deal(deal, configuration, context):
    # Deal processing logic
    pass

def process_partnership(partnership, configuration, context):
    # Partnership processing logic
    pass

if __name__ == '__main__':
    app.run(debug=True)

PHP with JWT Verification

<?php
// webhook.php
require_once 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

function handleWebhook() {
    // Get authorization header
    $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

    if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
        http_response_code(401);
        exit('Missing or invalid authorization header');
    }

    $token = substr($authHeader, 7); // Remove 'Bearer ' prefix

    try {
        // Verify JWT token using your integration UUID as the secret
        $decoded = JWT::decode($token, new Key($_ENV['INTEGRATION_UUID'], 'HS256'));

        error_log('Webhook context: ' . json_encode([
            'company' => $decoded->company_uuid,
            'user' => $decoded->user_uuid,
            'event' => $decoded->event_id,
            'api_key' => $decoded->api_key ?? null
        ]));

        // Get and process webhook payload
        $payload = json_decode(file_get_contents('php://input'), true);

        if (isset($payload['lead'])) {
            error_log("Processing lead: " . $payload['lead']['company_name']);
            processLead($payload['lead'], $payload['configuration'], $decoded);
        } elseif (isset($payload['deal'])) {
            error_log("Processing deal: " . $payload['deal']['label']);
            processDeal($payload['deal'], $payload['configuration'], $decoded);
        } elseif (isset($payload['partnership'])) {
            error_log("Processing partnership: " . $payload['partnership']['company_name']);
            processPartnership($payload['partnership'], $payload['configuration'], $decoded);
        }

        http_response_code(200);
        echo 'OK';

    } catch (Exception $e) {
        error_log('JWT verification failed: ' . $e->getMessage());
        http_response_code(401);
        exit('Invalid token');
    }
}

function processLead($lead, $configuration, $context) {
    // Use context for company-specific processing
    error_log("Processing lead for company: " . $context->company_uuid);

    // Access configuration settings
    foreach ($configuration as $config) {
        if ($config['id'] === 'lead_integration_settings') {
            foreach ($config['selected'] as $setting) {
                if ($setting['id'] === 'unique_field_lead') {
                    error_log("Unique field mapping: " . $setting['value']);
                }
            }
        }
    }

    // Your integration logic here
}

function processDeal($deal, $configuration, $context) {
    // Deal processing logic
}

function processPartnership($partnership, $configuration, $context) {
    // Partnership processing logic
}

handleWebhook();
?>

Security Best Practices

1. Always Verify JWT Tokens

// ❌ DON'T: Process webhooks without verification
app.post("/webhook", (req, res) => {
  const payload = req.body;
  processEvent(payload); // Dangerous!
  res.send("OK");
});

// ✅ DO: Always verify JWT tokens first
app.post("/webhook", (req, res) => {
  const token = extractBearerToken(req);

  try {
    const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);
    processEvent(req.body, decoded);
    res.send("OK");
  } catch (error) {
    return res.status(401).send("Invalid token");
  }
});

2. Validate Token Expiration

JWT tokens include expiration times to prevent replay attacks:
function verifyToken(token, integrationUuid) {
  try {
    const decoded = jwt.verify(token, integrationUuid);

    // JWT library automatically checks expiration,
    // but you can add custom validation
    if (decoded.exp < Math.floor(Date.now() / 1000)) {
      throw new Error("Token expired");
    }

    return decoded;
  } catch (error) {
    console.error("Token verification failed:", error.message);
    throw error;
  }
}

3. Validate Company and User Context

Use the JWT payload to validate requests:
function validateWebhookContext(decoded, expectedCompany = null) {
  // Check required fields
  if (!decoded.company_uuid || !decoded.user_uuid || !decoded.event_id) {
    throw new Error("Missing required token fields");
  }

  // Optionally validate company
  if (expectedCompany && decoded.company_uuid !== expectedCompany) {
    throw new Error("Company mismatch");
  }

  // Validate event types
  const validEventTypes = [
    "partner_created",
    "partner_updated",
    "partner_contact_created",
    "lead_created",
    "lead_updated",
    "lead_deleted",
    "deal_created",
    "deal_updated",
    "deal_deleted",
    "lead_note_created",
    "deal_note_created",
  ];

  if (!validEventTypes.includes(decoded.event_id)) {
    throw new Error(`Invalid event type: ${decoded.event_id}`);
  }

  return true;
}

4. Secure Integration UUID Storage

Store your integration UUID securely:
# Environment variables
INTEGRATION_UUID=your_integration_uuid_from_journeybee

# Docker secrets
docker secret create integration-uuid /path/to/uuid/file

# Kubernetes secrets
kubectl create secret generic webhook-secret --from-literal=integration_uuid=your_uuid

5. Implement Idempotency

Use event IDs to prevent duplicate processing:
const processedEvents = new Set();

app.post("/webhook", async (req, res) => {
  const token = extractBearerToken(req);
  const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);

  // Check for duplicate events
  const eventKey = `${decoded.company_uuid}:${decoded.event_id}:${
    req.body.lead?.uuid || req.body.deal?.uuid || req.body.partnership?.uuid
  }`;

  if (processedEvents.has(eventKey)) {
    console.log(`Duplicate event ignored: ${eventKey}`);
    return res.status(200).send("Already processed");
  }

  // Process event
  await processEvent(req.body, decoded);

  // Mark as processed
  processedEvents.add(eventKey);
  res.status(200).send("OK");
});

6. Validate Payload Structure

function validateWebhookPayload(payload, eventType) {
  // Validate based on event type
  switch (eventType) {
    case "lead_created":
    case "lead_updated":
      if (!payload.lead || !payload.lead.uuid || !payload.lead.company_name) {
        throw new Error("Invalid lead payload structure");
      }
      break;

    case "deal_created":
    case "deal_updated":
      if (!payload.deal || !payload.deal.uuid || !payload.deal.label) {
        throw new Error("Invalid deal payload structure");
      }
      break;

    case "partner_created":
    case "partner_updated":
      if (
        !payload.partnership ||
        !payload.partnership.uuid ||
        !payload.partnership.company_name
      ) {
        throw new Error("Invalid partnership payload structure");
      }
      break;

    default:
      throw new Error(`Unknown event type: ${eventType}`);
  }

  // Validate configuration array
  if (!Array.isArray(payload.configuration)) {
    throw new Error("Configuration must be an array");
  }

  return true;
}

7. Implement Rate Limiting

const rateLimit = require("express-rate-limit");

const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000, // Limit each IP to 1000 requests per windowMs
  message: "Too many webhook requests",
  standardHeaders: true,
  legacyHeaders: false,
});

app.use("/webhook", webhookLimiter);

8. Log Security Events

app.post("/webhook", (req, res) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    console.warn("Webhook security: Missing authorization header");
    return res.status(401).send("Missing authorization");
  }

  try {
    const token = authHeader.split("Bearer ")[1];
    const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);

    console.info(
      `Webhook security: Valid request from company ${decoded.company_uuid} for event ${decoded.event_id}`
    );
    // Process webhook...
  } catch (error) {
    console.error(
      `Webhook security: Token verification failed - ${error.message}`
    );
    return res.status(401).send("Invalid token");
  }
});

Testing Authentication

Test with cURL

# Generate a test JWT token (replace with your integration UUID)
JWT_TOKEN="your-test-jwt-token"

curl -X POST https://your-webhook-url.com/webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -d '{
    "lead": {
      "uuid": "lead_test_123",
      "company_name": "Test Company",
      "email": "test@company.com"
    },
    "configuration": []
  }'

Generate Test Tokens

For development testing, you can generate test JWT tokens:
const jwt = require("jsonwebtoken");

const testToken = jwt.sign(
  {
    company_uuid: "company_test_123",
    user_uuid: "user_test_123",
    event_id: "lead_created",
    api_key: "test_api_key",
    external_settings: {
      authorisation: [],
    },
  },
  "your-integration-uuid"
);

console.log("Test JWT token:", testToken);

Webhook Integration Setup

To get your integration UUID and configure webhooks:
  1. Go to Integration Settings: Navigate to Settings → Developed Integrations in JourneyBee
  2. Create Integration: Click “Create New Integration”
  3. Get Integration UUID: Copy your unique integration UUID (this is your JWT secret)
  4. Configure Webhook URL: Set your webhook endpoint URL
  5. Test Connection: Use the built-in webhook tester

Next Steps