1. Home
  2. Docs
  3. Webhooks

Webhooks

Pong posts new messages to your URL as they happen — one JSON shape for every channel, signed with your webhook secret.

Webhooks are part of the Premium plan.

Set up

  1. Desk → Settings → Webhooks: enter an HTTPS URL.
  2. Choose which message types to send: visitor (default), agent, AI, system, and contact form bot (bot, off by default).
  3. Copy the signing secret and press Send test event.

Payload

POST /your/endpoint
content-type: application/json
X-Pong-Signature: t=1762862400,v1=5f2b9c...

{
  "version": 1,
  "eventName": "chatFragment",
  "eventId": "e4b1c8a2f5d97b3c6e0a1f2d8b4c7e95",
  "createdAt": "2026-11-11T12:00:00.000Z",
  "client": {
    "id": "3f9c2a1e7b8d4c60a5e2b7f1c9d03e4a",
    "chatId": "b71e0c94d2a35f68e1c7a09b4d2f6e13",
    "verified": true,
    "name": "Jane Doe",
    "email": "jane@acme.com",
    "phone": "+372 5555 1234",
    "fields": { "company": "Acme" },
    "notes": null,
    "groupId": null,
    "banned": false,
    "integrationData": { "plan": "Pro" },
    "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.4 },
    "avatar": "https://files.pong.ee/...",
    "referrer": "https://www.google.com/",
    "landing": "https://acme.com/pricing"
  },
  "messages": [
    {
      "id": "8a4d1f2e9c0b7a6354e1d2c3b4a59687",
      "type": "visitor",
      "channel": "web",
      "text": "Hi! Can I change my plan mid-month?",
      "attachments": [],
      "agentId": null,
      "createdAt": "2026-11-11T12:00:00.000Z"
    }
  ]
}
eventNameWhen
chatStartedThe first message of a conversation
chatFragmentEvery later message
testThe test button in settings (client is null)
  • messages is always an array.
  • client.id is always the primary id, after any merges.
  • Times are ISO 8601 strings here (REST uses Unix milliseconds).
  • channel is the only channel-specific field: web or email. An email to your support inbox arrives as a visitor message with channel: "email", its text is the body without the quoted history, starting with the subject in bold when the email starts a new thread. The first email in a person’s email conversation fires chatStarted.
  • Messages sent by triggers are agent messages, signed with the agent chosen in the trigger.
  • client.phone and client.fields carry what the visitor entered in the contact form; fields is keyed by the keys you set for your custom fields in Settings → Contact form.

Verify the signature

The header is t=<unix seconds>,v1=<hex>, where v1 = HMAC-SHA256(webhookSecret, t + "." + rawBody). Use the raw request body, before JSON parsing, and reject requests older than 5 minutes.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyPong(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

PHP

<?php
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_PONG_SIGNATURE'] ?? ''), $sig);
$ok = abs(time() - (int)($sig['t'] ?? 0)) <= 300
   && hash_equals(hash_hmac('sha256', $sig['t'] . '.' . $raw, PONG_WEBHOOK_SECRET), $sig['v1'] ?? '');
if (!$ok) { http_response_code(401); exit; }
$event = json_decode($raw);

Python

import hashlib, hmac, time

def verify_pong(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    if abs(time.time() - int(parts['t'])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])

Delivery and retries

  • Answer with any 2xx within 10 seconds. Anything else is retried with growing pauses (30 s, 1 min, 2 min … up to 1 h), up to 10 attempts.
  • Delivery is at least once: the same event can arrive twice. Deduplicate by eventId or messages[].id.
  • Order is not guaranteed; sort by messages[].createdAt if it matters.

← REST API