Webhooks

Rather than polling, register an endpoint and VyaparPing will POST to it when things happen in your account.

Events

EventFires when
message.receivedAn inbound message arrives from a contact
message.status_updatedA message you sent changed delivery status
conversation.createdA new conversation is opened for a contact

Managing endpoints

All under scope webhooks:manage.

POST/api/v1/webhooksscope: webhooks:manage
Register
curl -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_…" } }
The response includes secret in plaintext exactly once — store it to verify signatures. VyaparPing keeps only an encrypted copy and can never show it again.
GET/api/v1/webhooksscope: webhooks:manage

List your endpoints (never returns the secret).

GET/api/v1/webhooks/{id}scope: webhooks:manage
PATCH/api/v1/webhooks/{id}scope: webhooks:manage

Update url, events, or is_active — re-enabling clears the failure counter.

DELETE/api/v1/webhooks/{id}scope: webhooks:manage

Delivery 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.

Envelope
{
  "id": "8f3c…",
  "event": "message.received",
  "occurred_at": "2026-07-01T12:00:00.000Z",
  "account_id": "…",
  "data": { /* per-event, see below */ }
}
data by event
// 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).

Node.js
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).

Target restrictions (SSRF). The 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.