Skip to content

Webhook Payload Format — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Webhook Payload Format. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn webhook payload format standards: CloudEvents, JSON structure, versioning, signature fields, metadata conventions, and designing extensible payloads for consumer compatibility.

What You Learn

You will learn how to design webhook payloads that are self-describing, versioned, extensible, and compatible with the CloudEvents specification. You will understand metadata fields, data envelopes, and how to evolve payloads without breaking consumers.

Why It Matters

A well-designed payload format prevents integration breakage when adding fields, supports multiple consumer versions simultaneously, and follows industry standards that consumers already understand. Bad payload design causes Parsing errors, silent data loss, and brittle integrations.

Real-World Use

DodaTech's webhook system follows the CloudEvents specification across all 15 event types. The standardized format allows 200+ integration partners to build consumers without per-event-type documentation. A single parser handles all Webhooks.

CloudEvents Standard

// CloudEvents compliant webhook payload
const cloudEventPayload = {
    specversion: '1.0',
    id: 'wh_abc123def456',
    source: '/com/dodatech/antivirus/threats',
    specversion: '1.0',
    type: 'com.dodatech.threat.detected',
    datacontenttype: 'application/json',
    dataschema: 'https://api.dodatech.com/schemas/threat.json',
    subject: 'endpoint-445',
    time: '2026-06-28T12:00:00Z',
    data: {
        threatId: 'thr_789',
        type: 'ransomware',
        severity: 'critical',
        endpoint: 'endpoint-445',
        detectedAt: '2026-06-28T12:00:00Z',
    },
};

Expected output: specversion tells consumers which CloudEvents version to use. id is unique. source identifies the origin. type uses reverse-DNS notation for namespacing. data contains the event-specific payload.

Custom Payload Design

// Well-designed custom payload
const webhookPayload = {
    // Metadata
    webhookId: 'wh_abc123',
    event: 'order.created',
    version: '2.1',
    createdAt: '2026-06-28T12:00:00Z',
    environment: 'production',

    // Signature (in header typically, but can be in body)
    signature: 'sha256=abc123def456...',

    // Source information
    source: {
        system: 'order-service',
        instance: 'ord-123',
        region: 'us-east-1',
    },

    // Event data
    data: {
        orderId: 'ord_456',
        customerId: 'cus_789',
        items: [
            { productId: 'prod_1', quantity: 2, price: 2500 },
            { productId: 'prod_2', quantity: 1, price: 5000 },
        ],
        total: 10000,
        currency: 'USD',
        status: 'confirmed',
    },

    // Extension fields for future compatibility
    extensions: {
        featureFlags: ['express-checkout', 'loyalty-points'],
    },
};

Expected output: Metadata separates identity (webhookId), routing (event, source), and versioning. Data is the core payload. Extensions allow adding optional fields without breaking existing parsers.

Payload Versioning

// Version 1.0 payload
const v1Payload = {
    webhookId: 'wh_001',
    event: 'user.created',
    version: '1.0',
    data: {
        userId: 1,
        name: 'Alice',
        email: 'alice@example.com',
    },
};

// Version 2.0 payload (added phone, changed userId to string)
const v2Payload = {
    webhookId: 'wh_002',
    event: 'user.created',
    version: '2.0',
    data: {
        userId: 'user_002',
        name: 'Bob',
        email: 'bob@example.com',
        phone: '+1234567890',
    },
};

// Consumer handles both versions
function handleUserCreated(payload) {
    const { version, data } = payload;

    if (version === '1.0') {
        // Legacy: userId is number
        return processLegacyUser({
            id: `user_${data.userId}`,
            name: data.name,
            email: data.email,
        });
    }

    if (version.startsWith('2.')) {
        // Current: userId is string, phone available
        return processUser({
            id: data.userId,
            name: data.name,
            email: data.email,
            phone: data.phone || null,
        });
    }
}

Expected output: Version field allows the consumer to parse differently based on format version. Old consumers Process v1 unchanged. New consumers use v2 features. Backward-compatible changes use minor versions.

Array vs Single Event

// Single event (per webhook)
const singleEvent = {
    webhookId: 'wh_001',
    event: 'order.created',
    data: { orderId: 'ord_001' },
};

// Batch events (multiple in one webhook)
const batchEvent = {
    webhookId: 'wh_002',
    event: 'orders.created',
    data: [
        { orderId: 'ord_002' },
        { orderId: 'ord_003' },
        { orderId: 'ord_004' },
    ],
};

// Consumer handles both
function processWebhook(payload) {
    const events = Array.isArray(payload.data)
        ? payload.data
        : [payload.data];

    for (const event of events) {
        processSingleEvent(payload.event, event);
    }
}

Expected output: Single events are simpler for real-time processing. Batch events are more efficient for high-volume scenarios. The consumer normalizes both formats by wrapping single events in an array.

Payload Size Limits

// Payload size check
function validatePayload(payload) {
    const payloadStr = JSON.stringify(payload);
    const sizeBytes = Buffer.byteLength(payloadStr, 'utf8');

    console.log(`Payload size: ${(sizeBytes / 1024).toFixed(1)} KB`);

    if (sizeBytes > 256 * 1024) {
        console.error('Payload exceeds 256KB limit');
        return false;
    }

    // Truncate large fields if necessary
    if (payload.data && typeof payload.data.description === 'string') {
        if (payload.data.description.length > 1000) {
            payload.data.description =
                payload.data.description.slice(0, 1000) + '...';
        }
    }

    return true;
}

// Test
const largePayload = {
    webhookId: 'wh_003',
    event: 'report.generated',
    data: {
        description: 'A'.repeat(5000), // 5KB text
        largeArray: new Array(10000).fill('item'), // ~60KB
    },
};
validatePayload(largePayload);

Expected output: Most providers enforce 256KB-1MB payload limits. Large payloads are truncated or rejected. Keep webhook payloads under 100KB for reliable delivery.

Common Mistakes

1. No Version Field

Without a version field, any format change breaks all consumers. Add version from day one. Use semver (major.minor). Breaking changes increment major version.

2. Inconsistent Date Formats

One payload uses ISO 8601, another uses Unix timestamp, another uses US date format. Standardize all dates to ISO 8601 (2026-06-28T12:00:00Z). Document the format.

3. Nested Data Without Schema

Deeply nested data without documented schemas causes parsing errors. Define JSON Schema for each event type. Validate payloads against schemas before sending.

4. No Nullable Field Handling

Consumers crash on null where they expect an object, or missing fields where they expect strings. Use default values. Document which fields are nullable. Include empty arrays instead of null.

5. Mixing Types in Fields

A field is a string in v1, number in v2, and array in v3. Consumers cannot handle three different types for one field. Use separate fields (userId, userIds) instead of changing types.

Practice Questions

1. What is the purpose of the specversion field in CloudEvents?

It identifies the CloudEvents specification version. Consumers use it to determine parsing rules. Different spec versions may have different required fields or semantics.

2. How do you evolve a webhook payload without breaking consumers?

Add optional fields. Never remove fields. Use version field for breaking changes. Consumers check version and parse accordingly. Old consumers ignore unknown fields.

3. What is the recommended maximum webhook payload size?

256KB to 1MB depending on the provider. Keep payloads under 100KB for reliable delivery. Use references (URLs) for large data instead of embedding it.

4. Why use reverse-DNS for event type names?

Reverse DNS prevents naming conflicts. com.stripe.payment.succeeded is globally unique. payment.succeeded could conflict with another provider's event. Reverse DNS is the CloudEvents standard.

Challenge

Design a webhook payload format for a file processing service. Include events for file.uploaded, file.optimized, file.error. Follow CloudEvents spec. Include versioning. Design for sizes up to 10MB (use URLs for large data). Create JSON Schema for each event type.

FAQ

Should webhook payload be JSON or XML?

JSON is the industry standard. It is simpler, smaller, and supported by every modern programming language. XML is rare in new webhook implementations.

How do I handle binary data in webhook payloads?

Do not embed binary data. Upload the binary to a storage service (S3, Cloudflare R2) and include the URL in the webhook payload. The consumer downloads the binary asynchronously.

What is the difference between webhook headers and body payload?

Headers contain transport metadata: signature, content type, webhook ID. Body contains the event payload. Headers are easier to inspect without parsing the full body.

Should I include the signature in the body or headers?

Headers are preferred. Inspecting headers does not require parsing the body. Some consumers store raw headers and need headers available before body parsing.

How do I document webhook payloads for developers?

Provide JSON Schema for each event type. Host schemas at stable URLs. Include example payloads in documentation. Version the schemas. Use OpenAPI or AsyncAPI for API documentation.

Mini Project: Payload Validator

Build a webhook payload validator tool that: accepts a payload format definition (JSON Schema), tests payloads against the schema, validates dates and field types, checks size limits, suggests improvements, and generates documentation markdown.

What's Next

Now that you know how to design payloads, learn how to sign and verify webhooks with HMAC to ensure authenticity and integrity.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro