Home/Help Center/Payload reference

Integrations

Webhook Events and Payload Reference

Every webhook event VGraple CRM can send, the envelope shape, and a real JSON example for message, contact, lead, broadcast, conversation and form events.

By Chirag Darji · Updated 1 Sept 2026 · 8 min read

On this page
  1. Before you start
  2. The envelope every event shares
  3. Message events
  4. Contact events
  5. Lead events
  6. Broadcast events
  7. Conversation events
  8. Form events
  9. What you will see
  10. Settings and options
  11. Troubleshooting
Outbound webhooks in VGraple CRM with the events each endpoint subscribes to and its delivery status

Every webhook delivery from VGraple CRM shares one envelope shape (an id, a type, a created timestamp and a data object) wrapped around 15 possible event types, from a message arriving to a broadcast finishing. This reference documents each event's trigger condition and its exact payload fields, so you can parse a delivery without guessing at field names.

Before you start

  • This page assumes you already have an endpoint receiving deliveries. See configuring outbound webhooks if you have not created one yet.
  • Every payload should be verified with the X-Signature-256 header before you trust it; see verifying webhook signatures.
  • 11 of these 15 events are selectable directly in Settings > Webhooks; the four broadcast lifecycle events not yet in that checkbox list (broadcast.started, broadcast.paused, broadcast.failed, broadcast.reply) can still be subscribed to via POST /api/v1/hooks with an API key, described below.

The envelope every event shares

Every delivery, regardless of type, is a JSON object with this shape:

API keys in VGraple CRM with prefix, creation date and last use

{
  "id": "evt_9f2a7c1b4e8d0a3f",
  "type": "message.sent",
  "created": 1798329600,
  "data": { }
}

id is unique per delivery attempt (retries of the same underlying event reuse a related but distinct id). type matches the event name exactly, so you can route on it with a simple switch statement. created is a Unix timestamp in seconds. data holds the event-specific object described below.

Message events

message.received fires when a customer's inbound message is saved to a conversation. message.sent fires when an agent, an automation rule or an API call sends an outbound message; broadcast recipients do not fire individual message.sent events, subscribe to broadcast.completed for campaign-level confirmation instead.

{
  "id": "evt_9f2a7c1b4e8d0a3f",
  "type": "message.sent",
  "created": 1798329600,
  "data": {
    "message": {
      "id": "msg_01H...",
      "wa_message_id": "wamid.HBgL...",
      "body": "Your appointment is confirmed for Saturday at 3 PM.",
      "type": "text",
      "direction": "outbound",
      "status": "sent",
      "channel": "whatsapp",
      "template_name": null,
      "error": null,
      "is_forwarded": false,
      "media": null,
      "reply_to": null,
      "wa_timestamp": "2026-08-26T09:00:00.000Z",
      "created_at": "2026-08-26T09:00:00.000Z",
      "updated_at": "2026-08-26T09:00:01.114Z"
    },
    "contact": { "id": "con_01H...", "name": "Priya Sharma", "phone": "+919876543210" },
    "conversation_id": "conv_01H..."
  }
}

message.received uses the identical message shape with "direction": "inbound".

This is the same message object GET /api/v1/messages returns, so whatever parses one parses the other. Four of its fields are worth calling out:

FieldWhat it carries
statusDelivery state: sent, delivered, read, failed. Webhooks fire once, at send or receipt, so a later status change is not re-delivered; poll the API if you need the final state.
errornull unless the platform rejected the message, otherwise { "code", "message" } with the provider's own code.
medianull, or { "mime", "caption", "url", "stored" }. The url needs your API key as a Bearer token, it is not public. "stored": false means the file still lives on Meta's servers and expires roughly 30 days after the message.
reply_tonull, or the quoted message in both id spaces: { "message_id", "wa_message_id" }.

channel tells you which surface the message came from: whatsapp, messenger, instagram or web_chat. wa_timestamp is the messaging platform's own clock and created_at is ours; they differ by the delivery delay on live traffic.

Contact events

contact.created fires from every real contact-creation path: an inbound WhatsApp or Meta DM, a manual add, a CSV import, a lead auto-create, a Calendly booking, a comment DM, a template-send auto-create, or the public API. It deliberately does not fire for synthetic web-chat placeholder contacts that have not yet identified themselves. contact.opted_out fires when a contact opts out of messaging.

{
  "id": "evt_3c8f1a90bd42e771",
  "type": "contact.created",
  "created": 1798329600,
  "data": {
    "contact": {
      "id": "con_01H...",
      "wa_id": "919876543210",
      "name": "Priya Sharma",
      "phone": "+919876543210",
      "email": "[email protected]",
      "source": "import",
      "source_channel": "whatsapp",
      "opted_in": true,
      "created_at": "2026-08-26T09:00:00.000Z"
    }
  }
}

Lead events

lead.created fires from all six lead sources: Meta lead ads, WhatsApp forms, VGraple web forms, web chat, comments, and the API. lead.stage_changed fires when a lead moves to a different pipeline stage, whether by dragging it on the Kanban board or editing it in the detail panel; bulk housekeeping moves (like a fallback move after a stage is deleted) intentionally do not fire it.

{
  "id": "evt_7d21fa30ce998c11",
  "type": "lead.created",
  "created": 1798329600,
  "data": {
    "lead": {
      "id": "lead_01H...",
      "name": "Asha Patel",
      "phone": "+919876543210",
      "email": "[email protected]",
      "source": "meta_lead_ad",
      "stage_id": "stage_01H...",
      "stage_name": "New",
      "pipeline_id": "pipe_01H...",
      "contact_id": "con_01H...",
      "form_name": "Monsoon Offer Form",
      "campaign_name": "Monsoon Sale 2026",
      "created_at": "2026-08-26T09:00:00.000Z"
    }
  }
}

Broadcast events

Broadcast events are campaign-level, not per-recipient, except broadcast.reply, which fires once per recipient who responds. A campaign reaching 10,000 contacts still fires exactly one broadcast.completed event, so your endpoint never absorbs a burst sized to the audience.

EventFires when
broadcast.startedA broadcast campaign begins sending
broadcast.pausedA broadcast auto-parks (template pause, portfolio pacing, red quality rating, quota reached, or a high early failure rate)
broadcast.completedA broadcast finishes, including crash-recovery completion after a deploy
broadcast.failedA broadcast ends in a failed state
broadcast.replyA recipient replies to a broadcast
{
  "id": "evt_a1f9c02de5b73401",
  "type": "broadcast.completed",
  "created": 1798329600,
  "data": {
    "broadcast": {
      "id": "bc_01H...",
      "name": "Monsoon Flash Sale",
      "status": "completed",
      "total_recipients": 3200,
      "sent": 3180,
      "delivered": 3050,
      "failed": 20
    }
  }
}

Conversation events

conversation.assigned fires on a manual assignment or an SLA-driven auto-assign. conversation.resolved fires when an agent marks a conversation resolved (CSAT survey mechanics do not re-fire it). conversation.reopened fires on an agent reopening a resolved conversation, or a customer sending a new message that reopens one (a CSAT rating tap does not count).

{
  "id": "evt_5b0e21ac8f34d902",
  "type": "conversation.assigned",
  "created": 1798329600,
  "data": {
    "conversation_id": "conv_01H...",
    "contact": { "id": "con_01H...", "name": "Priya Sharma" },
    "assigned_to": { "id": "usr_01H...", "name": "Rohan" }
  }
}

Form events

form.submitted fires when a VGraple web form is submitted, the same form-builder feature covered in building web forms.

{
  "id": "evt_e40b19c3a276f508",
  "type": "form.submitted",
  "created": 1798329600,
  "data": {
    "form_id": "form_01H...",
    "form_name": "Contact Us",
    "lead_id": "lead_01H...",
    "is_new_lead": true,
    "name": "Asha Patel",
    "email": "[email protected]",
    "phone": "+919876543210",
    "page_url": "https://example.com/contact",
    "submitted_at": "2026-08-26T09:00:00.000Z"
  }
}

What you will see

A well-formed delivery always parses as the envelope above; data's inner shape depends only on type, and shares fields consistently across events for the same underlying resource (every message-related event uses the same message object, for instance). There is no versioning field in the envelope today, so treat any new field VGraple CRM adds to an existing payload as additive rather than a breaking change.

Settings and options

FieldApplies toNotes
idEvery eventUnique per delivery attempt, prefixed evt_
typeEvery eventMatches one of the 15 catalog names exactly
createdEvery eventUnix timestamp, seconds
dataEvery eventEvent-specific object, documented above per event family
source (Settings > Webhooks list, not in the payload)Endpoint metadata"zapier" if created by a Zap turning on, otherwise unset

Troubleshooting

SymptomLikely causeFix
A field you expected is missing from a payloadThe field is only present when applicable (for example template_name is null on a non-template send)Handle null and missing optional fields defensively rather than assuming every field is always populated
You subscribed to broadcast.reply but see no deliveriesNo recipient has replied yet, or the reply landed outside the tracked broadcast attribution windowConfirm the broadcast actually has replies on its detail page in the UI before assuming the webhook is broken
You want broadcast.started but it is not in the Settings checkbox listThe Settings UI form currently exposes 11 of the 15 catalog eventsUse POST /api/v1/hooks with your API key and "events": ["broadcast.started"] in the body; see the REST API quickstart
Two deliveries for what looks like the same eventWebhooks are delivered at-least-once; a retry after a transient failure can occasionally overlap with a fresh deliveryDeduplicate on id in your own system if exact-once processing matters to you
contact.created never fires for your web chat widget visitorsThis is by design, not a bugWait for the visitor to identify themselves (name, phone or email); a placeholder web-chat contact never fires the event

Once you can parse a payload, confirm it is authentic with signature verification, and see configuring webhooks to manage which endpoints receive which events.

Frequently asked questions

What does the outer envelope of every webhook payload look like?
A JSON object with four fields: id (prefixed evt_), type (matching the event name exactly, for example contact.created), created (a Unix timestamp in seconds), and data (an object holding the event-specific payload, for example a contact or message record).
How many event types are there in total?
15, across messages, contacts, leads, broadcasts, conversations and forms. 11 of them are selectable directly from the Settings > Webhooks endpoint form; the remaining four, all broadcast lifecycle events, can be subscribed to by calling POST /api/v1/hooks with your API key.
Do broadcast events fire once per recipient?
No. A campaign reaching 10,000 contacts fires one broadcast.completed event at the campaign level, not 10,000 individual events. broadcast.started, broadcast.paused, broadcast.completed and broadcast.failed are all campaign-level; broadcast.reply fires once per recipient who actually replies.
Does contact.created fire for every visitor who opens my website chat widget?
No. Synthetic web-chat placeholder contacts (created before a visitor identifies themselves) deliberately do not fire this event, since they are not yet a real, contactable person. It fires from every real creation path: an inbound WhatsApp message, a manual add, a CSV import, a lead auto-create, a Calendly booking, a comment DM, or the public API.
What fields does a lead.created payload carry?
id, name, phone, email, source, stage_id, stage_name, pipeline_id, contact_id, form_name, campaign_name and created_at. The source field tells you which of the six lead sources produced it: Meta lead ads, WhatsApp forms, VGraple web forms, web chat, comments, or the API.
Are message.received and message.sent payloads the same shape?
Yes. Both carry the identical message object, differing only in the direction field, plus the same contact and conversation_id alongside it. It is the same object GET /api/v1/messages returns, so an integration built against a polled sample behaves the same on a live delivery.
How do I get the image or document attached to a message?
The message object carries a media block whose url is an authenticated download link. Fetch it with your API key as a Bearer token; it is not a public URL. When media is stored is false the file is still on Meta servers and expires about 30 days after the message, so download it promptly.
Can I tell from a webhook that a send failed?
Yes. status carries the delivery state and error is populated only when the platform rejected the message, with the provider code and message. A null error with a non-final status means still in flight, not failed.
How do I subscribe to the four events not in the Settings checkbox list?
Call POST /api/v1/hooks with your API key, an https URL, and an events array that includes broadcast.started, broadcast.paused, broadcast.failed or broadcast.reply; the API validates against the full 15-event catalog even though the Settings page's form currently offers only 11.

Run your WhatsApp on VGraple CRM

Free forever plan, official Meta WhatsApp Business API, set up in 15 minutes. No card needed.