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_idUnique identifier for this webhook delivery. Use for idempotency.
event_typeThe type of event (e.g., user.created, payment.succeeded).
application_idThe customer application this webhook belongs to. Useful for multi-tenant routing.
timestampWhen the webhook was created (ISO 8601 format, UTC).
payloadYour 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.
| Limit | Value | Notes |
|---|---|---|
| Maximum payload size | 256 KB | After JSON serialization |
| Recommended size | < 64 KB | For optimal performance |
| String field length | Unlimited | Within payload size |
| Nesting depth | Unlimited | Within payload size |
| Array length | Unlimited | Within 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
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"
}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"
}Keep Payloads Self-Contained
Include all data needed to process the event without additional API calls. Balance this with payload size limits.
Use Consistent Field Names
Maintain consistent naming across all event types. Use snake_case for field names to match JSON API conventions.
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"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"
}
]
}