Skip to content

Webhook Payload Schemas

Payload structure, example payloads, HTTP headers, and HMAC signature verification for Vivreal webhooks

intermediate8 min readFor developers

Webhook Payload Schemas

Every webhook delivery from Vivreal follows a consistent JSON structure. This guide documents the top-level payload format, the HTTP headers Vivreal sends, and provides example payloads for each emitted event type.

General Payload Structure

The HTTP body Vivreal POSTs to your endpoint has this top-level structure:

{
  "id": "7b3f9c22-4e8d-4b1c-9f5a-8e2d1a6b4c87",
  "event": "content.created",
  "timestamp": "2026-03-15T14:30:00.000Z",
  "data": {
    // Event-specific document — see examples below.
    // For most events, this is the resource's MongoDB document, which itself
    // includes `groupID`. Use that field if you need to know which group
    // emitted the event.
  }
}
FieldTypeDescription
idstringUUID v4 generated per event. Use for deduplication — the same id may be delivered more than once if your endpoint returns a non-2xx response (SQS retries). Also sent as the X-Vivreal-Delivery header.
eventstringThe event type (see Event Types). Also sent as the X-Vivreal-Event header.
timestampstringISO 8601 timestamp of when the event was emitted.
dataobjectEvent-specific payload. Structure varies by event type — see Example Payloads below.

groupID / dbKey are not at the top level

Vivreal carries groupID and dbKey internally on the SQS message but does not include them at the top level of the HTTP delivery payload. For multi-tenant routing, read data.groupID (present on every resource document Vivreal emits). dbKey is not exposed to webhook consumers at all — it's an internal database routing key.

HTTP Headers

Each webhook request includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Vivreal-Signaturesha256=<hex> — HMAC-SHA256 hex digest of the raw request body, prefixed with sha256=
X-Vivreal-EventThe event type (same value as event in the body)
X-Vivreal-DeliveryPer-delivery identifier (use for idempotency / deduplication)
User-AgentVivreal-Webhooks/1.0

Header case on the wire

HTTP headers are case-insensitive on the wire. Node.js and most frameworks normalize incoming header names to lowercase, so req.headers['x-vivreal-signature'] is how you will read the signature in your handler. The canonical case in the docs is X-Vivreal-Signature.

Example Payloads

content.created

Fired when a new content item (collection object) is added to a collection.

{
  "id": "7b3f9c22-4e8d-4b1c-9f5a-8e2d1a6b4c87",
  "event": "content.created",
  "timestamp": "2026-03-15T14:30:00.000Z",
  "data": {
    "_id": "68f28012a3c1d4b001234567",
    "groupID": "68f27fec32e7acbb755c087e",
    "refID": "68f27fff9a2b4c0012345001",
    "objectValue": {
      "name": "Wireless Headphones",
      "price": 79.99
    },
    "author": "user@example.com",
    "publishDate": null,
    "archived": false
  }
}

content.updated

Fired when an existing content item is modified. The data field carries the updated collection object document as returned by the update service.

{
  "id": "8c4d0e34-5f9d-4c2d-a1b6-9f3e2c7d5e98",
  "event": "content.updated",
  "timestamp": "2026-03-15T15:00:00.000Z",
  "data": {
    "_id": "68f28012a3c1d4b001234567",
    "groupID": "68f27fec32e7acbb755c087e",
    "refID": "68f27fff9a2b4c0012345001",
    "objectValue": {
      "name": "Wireless Headphones Pro",
      "price": 99.99
    },
    "author": "user@example.com"
  }
}

content.deleted

{
  "id": "9d5e1f45-6a0e-4d3e-b2c7-a04f3d8e6fa9",
  "event": "content.deleted",
  "timestamp": "2026-03-15T15:30:00.000Z",
  "data": {
    "_id": "68f28012a3c1d4b001234567",
    "groupID": "68f27fec32e7acbb755c087e",
    "refID": "68f27fff9a2b4c0012345001"
  }
}

collection.created

Fired when a new collection group (schema / container) is created.

{
  "id": "ae6f2056-7b1f-4e4f-c3d8-b15a4e9f70ba",
  "event": "collection.created",
  "timestamp": "2026-03-15T16:00:00.000Z",
  "data": {
    "_id": "68f27fff9a2b4c0012345001",
    "groupID": "68f27fec32e7acbb755c087e",
    "name": "Products",
    "type": "standard",
    "hasMedia": true,
    "schema": {
      "name": { "type": "text", "required": true },
      "price": { "type": "decimal", "required": true }
    }
  }
}

integration.created

Fired when a new integration object is created (for example, a Stripe product synced into Vivreal).

{
  "id": "bf7036bd-8c2a-4f5a-d4e9-c26b5fa081cb",
  "event": "integration.created",
  "timestamp": "2026-03-15T17:00:00.000Z",
  "data": {
    "_id": "68f28099c5d6e7f001234abc",
    "groupID": "68f27fec32e7acbb755c087e",
    "platform": "stripe",
    "id": "prod_ABC123",
    "objectValue": {
      "name": "Wireless Headphones",
      "price": 7999,
      "description": "Studio-grade Bluetooth headphones",
      "productImage": "https://files.stripe.com/...",
      "default_price": "price_DEF456",
      "active": true,
      "livemode": true
    }
  }
}

Stripe price units

For Stripe-sourced integration objects, objectValue.price is in integer cents, not decimal dollars. See the Stripe Setup guide for the full field mapping.

Signature Verification

Always verify the X-Vivreal-Signature header before trusting the payload. The header value has the form sha256=<hex> — you must strip the prefix before comparing.

Node.js

import crypto from 'crypto';
import express from 'express';

const app = express();
const WEBHOOK_SECRET = process.env.VIVREAL_WEBHOOK_SECRET;

function verify(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
    return false;
  }
  const received = signatureHeader.slice('sha256='.length);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(received, 'hex');
  const b = Buffer.from(expected, 'hex');

  // timingSafeEqual requires equal-length buffers
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

app.post(
  '/webhooks/vivreal',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-vivreal-signature'];
    const deliveryId = req.headers['x-vivreal-delivery'];

    if (!verify(req.body, signature, WEBHOOK_SECRET)) {
      console.error('Webhook signature verification failed');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString('utf8'));
    console.log(`Received ${event.event} (delivery ${deliveryId})`);

    // Acknowledge receipt within 5 seconds
    res.status(200).json({ received: true });

    // Process asynchronously if needed
    processEventAsync(event).catch(console.error);
  },
);

Use express.raw() for signature verification

You must access the raw request body (not the parsed JSON) to compute the signature correctly. Using express.json() middleware before verification will alter the body and cause signature mismatches.

Python

import hmac
import hashlib

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    # Header format: "sha256=<hex>"
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    received = signature_header[len("sha256="):]

    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    # compare_digest accepts equal-length strings and runs in constant time
    return hmac.compare_digest(received, expected)

Handling Large Payloads

Most webhook payloads are small (under 10 KB). However, events for objects with rich text or many fields can be larger. Your endpoint should accept payloads up to 256 KB. If a payload would exceed that limit, the event is still delivered but rich field content may be truncated — always treat the webhook as a signal to fetch the full object via the API if you need the complete record.

Source of truth