Payloads & Metadata

Structure your webhook payloads effectively and add custom metadata for routing and debugging.

Payload Structure

Every webhook job has a payload containing the event data you want to send. Payloads are JSON objects that can contain any data structure your application needs.

// Basic webhook payload
const payload = {
  user_id: "usr_abc123",
  email: "alice@example.com",
  name: "Alice Johnson",
  created_at: "2026-01-20T10:30:00Z"
};

// Create webhook job with payload
const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_xyz789',
    event_type: 'user.created',
    payload: payload
  })
});

Payload is Delivered As-Is

Hook Mesh delivers your payload exactly as provided. No transformations or modifications are made to the data.

Complete Webhook Structure

When Hook Mesh delivers a webhook to your endpoint, it wraps your payload in a standard envelope with metadata:

{
  "event_id": "evt_abc123def456",
  "event_type": "user.created",
  "application_id": "app_xyz789",
  "timestamp": "2026-01-20T10:30:00.000Z",
  "payload": {
    "user_id": "usr_abc123",
    "email": "alice@example.com",
    "name": "Alice Johnson",
    "created_at": "2026-01-20T10:30:00Z"
  }
}

Envelope Fields:

event_id

Unique identifier for this webhook delivery. Use for idempotency.

event_type

The type of event (e.g., user.created, payment.succeeded).

application_id

The customer application this webhook belongs to. Useful for multi-tenant routing.

timestamp

When the webhook was created (ISO 8601 format, UTC).

payload

Your custom event data - this is what you provided when creating the job.

Payload Size Limits

Webhook payloads have a maximum size of 256 KB when serialized to JSON. This includes all nested objects, arrays, and strings.

LimitValueNotes
Maximum payload size256 KBAfter JSON serialization
Recommended size< 64 KBFor optimal performance
String field lengthUnlimitedWithin payload size
Nesting depthUnlimitedWithin payload size
Array lengthUnlimitedWithin payload size

Large Payloads Impact Performance

While 256 KB is the hard limit, smaller payloads (< 64 KB) deliver faster and are more reliable. Consider using URLs to reference large data instead of embedding it directly.

Handling Large Data

If your event data exceeds the payload size limit, use the reference pattern: send a URL or ID in the webhook, and let the consumer fetch the full data via API.

// ❌ Bad: Embedding large binary data
const payload = {
  order_id: "ord_123",
  invoice_pdf: "JVBERi0xLjQKJeLjz9MKNCAwIG9..." // 500 KB base64 string
};

// ✅ Good: Reference pattern
const payload = {
  order_id: "ord_123",
  invoice_url: "https://api.yourapp.com/v1/orders/ord_123/invoice.pdf",
  invoice_expires_at: "2026-01-27T10:30:00Z"
};

// Consumer fetches the data when needed
async function handleOrderCompleted(webhook) {
  const { order_id, invoice_url } = webhook.payload;

  // Fetch the large file only if needed
  const invoiceResponse = await fetch(invoice_url, {
    headers: {
      'Authorization': `Bearer ${customerApiKey}`
    }
  });

  const invoicePdf = await invoiceResponse.arrayBuffer();
  // Process the PDF...
}

Benefits of Reference Pattern

  • Smaller webhook payloads (faster delivery)
  • Consumers only fetch data when needed
  • Supports any file size or data volume
  • Can include expiring signed URLs for security

Payload Best Practices

1️⃣

Include Resource Identifiers

Always include IDs for the primary resources affected by the event. This allows consumers to fetch full details via API if needed.

{
  "user_id": "usr_123",
  "subscription_id": "sub_456",
  "plan_id": "plan_pro"
}
2️⃣

Use ISO 8601 Timestamps

Use standard ISO 8601 format (with timezone) for all dates and times. This prevents timezone confusion.

{
  "created_at": "2026-01-20T10:30:00.000Z",  // ✓ Good: ISO 8601 with timezone
  "expires_at": "2026-02-20T10:30:00.000Z"
}
3️⃣

Keep Payloads Self-Contained

Include all data needed to process the event without additional API calls. Balance this with payload size limits.

4️⃣

Use Consistent Field Names

Maintain consistent naming across all event types. Use snake_case for field names to match JSON API conventions.

5️⃣

Avoid Sensitive Data

Don't include passwords, API keys, or credit card numbers in webhook payloads. Use IDs that consumers can exchange for sensitive data via authenticated API calls.

credit_card_number: "4532..."
payment_method_id: "pm_abc123"
6️⃣

Version Your Payload Schema

As your events evolve, use versioned event types (e.g., user.created.v1, user.created.v2) or include a schema version field in the payload to maintain backward compatibility.

Custom Metadata

In addition to the payload, webhook jobs support a metadata field for storing arbitrary key-value data. Metadata is NOT sent to consumers—it's only visible in the Hook Mesh API.

// Create webhook with metadata
const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_xyz789',
    event_type: 'order.completed',
    payload: {
      order_id: 'ord_123',
      total: 99.99,
      currency: 'USD'
    },
    metadata: {
      // Metadata for internal tracking (not sent to consumers)
      source: 'mobile_app',
      user_agent: 'iOS App v2.1.0',
      region: 'us-west-2',
      trace_id: 'trace_abc123def456'
    }
  })
});

Common Metadata Use Cases:

Tracing and Debugging

Store trace IDs, request IDs, or correlation IDs to connect webhook deliveries to your application logs.

Source Tracking

Record where the event originated (e.g., web, mobile, API, admin panel) for analytics.

Internal Context

Store internal identifiers, tenant IDs, or deployment information for operations.

Filtering and Search

Use metadata fields to filter webhook jobs in the API or dashboard (e.g., by region, feature flag, or environment).

💡

Metadata vs Payload

Use payload for data your consumers need. Use metadata for internal tracking, debugging, and operations data that only you need to see.

Complete Example

Here's a complete example showing payload and metadata in different languages:

// Node.js - Creating a webhook with payload and metadata
import fetch from 'node-fetch';

async function notifySubscriptionUpgrade(
  userId: string,
  fromPlan: string,
  toPlan: string,
  traceId: string
) {
  const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      application_id: 'app_xyz789',
      event_type: 'subscription.upgraded',

      // Payload: data for consumers
      payload: {
        user_id: userId,
        subscription: {
          previous_plan: fromPlan,
          current_plan: toPlan,
          upgraded_at: new Date().toISOString(),
          next_billing_date: getNextBillingDate()
        },
        billing: {
          amount: 49.99,
          currency: 'USD',
          payment_method: 'pm_abc123'
        }
      },

      // Metadata: internal tracking only
      metadata: {
        source: 'billing_service',
        trace_id: traceId,
        upgraded_by: 'user_self_serve',
        feature_flag: 'new_upgrade_flow',
        deployment: 'v2.1.0'
      }
    })
  });

  const job = await response.json();
  console.log(`Webhook created: ${job.id}`);

  return job;
}

// Usage
await notifySubscriptionUpgrade(
  'usr_123',
  'basic',
  'pro',
  'trace_abc123def456'
);

function getNextBillingDate(): string {
  const date = new Date();
  date.setMonth(date.getMonth() + 1);
  return date.toISOString();
}
# Python - Creating a webhook with payload and metadata
import requests
import os
from datetime import datetime, timedelta

def notify_subscription_upgrade(
    user_id: str,
    from_plan: str,
    to_plan: str,
    trace_id: str
):
    response = requests.post(
        'https://api.hookmesh.com/v1/webhook-jobs',
        headers={
            'Authorization': f'Bearer {os.environ["HOOKMESH_API_KEY"]}',
            'Content-Type': 'application/json'
        },
        json={
            'application_id': 'app_xyz789',
            'event_type': 'subscription.upgraded',

            # Payload: data for consumers
            'payload': {
                'user_id': user_id,
                'subscription': {
                    'previous_plan': from_plan,
                    'current_plan': to_plan,
                    'upgraded_at': datetime.utcnow().isoformat() + 'Z',
                    'next_billing_date': get_next_billing_date()
                },
                'billing': {
                    'amount': 49.99,
                    'currency': 'USD',
                    'payment_method': 'pm_abc123'
                }
            },

            # Metadata: internal tracking only
            'metadata': {
                'source': 'billing_service',
                'trace_id': trace_id,
                'upgraded_by': 'user_self_serve',
                'feature_flag': 'new_upgrade_flow',
                'deployment': 'v2.1.0'
            }
        }
    )

    job = response.json()
    print(f'Webhook created: {job["id"]}')

    return job

# Usage
notify_subscription_upgrade(
    'usr_123',
    'basic',
    'pro',
    'trace_abc123def456'
)

def get_next_billing_date() -> str:
    next_month = datetime.utcnow() + timedelta(days=30)
    return next_month.isoformat() + 'Z'

Common Payload Patterns

Resource Created Events

{
  "user_id": "usr_123",
  "email": "alice@example.com",
  "name": "Alice Johnson",
  "created_at": "2026-01-20T10:30:00Z",
  "email_verified": false,
  "plan": "free"
}

Resource Updated Events

Include both previous and current values for changed fields:

{
  "user_id": "usr_123",
  "updated_at": "2026-01-20T15:45:00Z",
  "changes": {
    "email": {
      "previous": "alice@oldmail.com",
      "current": "alice@example.com"
    },
    "email_verified": {
      "previous": false,
      "current": true
    }
  }
}

Action Completed Events

{
  "payment_id": "pay_123",
  "user_id": "usr_456",
  "amount": 99.99,
  "currency": "USD",
  "status": "succeeded",
  "payment_method": {
    "type": "card",
    "last4": "4242"
  },
  "completed_at": "2026-01-20T12:00:00Z"
}

Batch Events

For multiple related items, include an array:

{
  "batch_id": "batch_123",
  "created_at": "2026-01-20T10:00:00Z",
  "total_items": 3,
  "items": [
    {
      "order_id": "ord_001",
      "status": "shipped",
      "tracking_number": "1Z999AA10123456784"
    },
    {
      "order_id": "ord_002",
      "status": "shipped",
      "tracking_number": "1Z999AA10123456785"
    },
    {
      "order_id": "ord_003",
      "status": "shipped",
      "tracking_number": "1Z999AA10123456786"
    }
  ]
}

Related Documentation