Webhooks
Rather than polling, register an endpoint and VyaparPing will POST to it when things happen in your account.
Events
| Event | Fires when |
|---|---|
message.received | An inbound message arrives from a contact |
message.status_updated | A message you sent changed delivery status |
conversation.created | A new conversation is opened for a contact |
Managing endpoints
All under scope webhooks:manage.
/api/v1/webhooksscope: webhooks:managecurl -X POST https://app.vyaparping.com/api/v1/webhooks \
-H "Authorization: Bearer vp_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/vyaparping", "events": ["message.received"] }'
# → 201 { "data": { "id": "…", "url": "…", "events": [...], "secret": "whsec_…" } }secret in plaintext exactly once — store it to verify signatures. VyaparPing keeps only an encrypted copy and can never show it again./api/v1/webhooksscope: webhooks:manageList your endpoints (never returns the secret).
/api/v1/webhooks/{id}scope: webhooks:manage/api/v1/webhooks/{id}scope: webhooks:manageUpdate url, events, or is_active — re-enabling clears the failure counter.
/api/v1/webhooks/{id}scope: webhooks:manageDelivery payload
Every delivery is a POST with this envelope; id is a unique per-delivery id you can dedupe on, and data varies by event.
{
"id": "8f3c…",
"event": "message.received",
"occurred_at": "2026-07-01T12:00:00.000Z",
"account_id": "…",
"data": { /* per-event, see below */ }
}// message.received
{ "conversation_id": "…", "contact_id": "…", "whatsapp_message_id": "wamid.…", "content_type": "text", "text": "Hi 👋" }
// conversation.created
{ "conversation_id": "…", "contact_id": "…" }
// message.status_updated
{ "whatsapp_message_id": "wamid.…", "conversation_id": "…", "status": "delivered" }Headers: X-VyaparPing-Event, X-VyaparPing-Webhook-Id, and X-VyaparPing-Signature (plus deprecated X-Wacrm-* aliases carrying the same values, for receivers built before the rebrand — safe to ignore for new integrations).
Verifying the signature
X-VyaparPing-Signature: t=<unix_seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "t.rawBody") (t and the raw body joined with a dot). Recompute it over the raw request body and compare in constant time; reject if t is more than a few minutes old (replay protection).
const [, t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/);
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));Delivery semantics
Delivery is best-effort: a single attempt per event with a short timeout, and redirects are not followed. Because providers re-send and re-order status callbacks, the same message.status_updated event may arrive more than once or out of order — dedupe on id and don't assume ordering.
Each consecutive failure increments a failure counter; after enough consecutive failures the endpoint is auto-disabled (is_active: false) — re-enable it with PATCH (which resets the counter).
url must be https:// and resolve to a public address — requests to localhost, private/RFC1918 ranges, link-local addresses (including cloud metadata endpoints), and similar internal targets are refused at delivery time.