1. Home
  2. Docs
  3. REST API

REST API

Read clients and their messages, update client data and post messages from your backend. JSON in, JSON out.

The REST API is part of the Premium plan.

Base URL and auth

https://app.pong.ee/api/
Authorization: Pong.Simple <publicKey>:<restSecret>

Both keys are in Settings → API keys. Requests without a valid header get 401. Each workspace may make 120 requests per minute; above that you get 429 — wait and retry.

curl https://app.pong.ee/api/clients/3f9c2a1e7b8d4c60a5e2b7f1c9d03e4a \
  -H "Authorization: Pong.Simple $PONG_PUBLIC_KEY:$PONG_REST_SECRET"

Endpoints

MethodPathWhat
GET/api/clients/{id}One client
GET/api/clients/{id}/messagesThe client’s full message history, oldest first
PUT/api/clients/{id}Update notes, group, ban state and custom data
POST/api/messages/Send a message to a client as an agent
POST/api/pushedMessages/Send a message as the workspace, without an agent

Ids are 32-character hex strings. Client ids come from webhooks (client.id) and from the widget (Pong('getState').clientId). After two clients are merged, the old id keeps working and resolves to the merged one.

GET /api/clients/{id}

{
  "id": "3f9c2a1e7b8d4c60a5e2b7f1c9d03e4a",
  "chatId": "b71e0c94d2a35f68e1c7a09b4d2f6e13",
  "verified": true,
  "name": "Jane Doe",
  "email": "jane@acme.com",
  "phone": "+372 5555 1234",
  "fields": { "company": "Acme" },
  "contactAt": 1762862300000,
  "emailInfo": {
    "mailbox": "support@acme.com",
    "received": 3,
    "mailer": "Apple Mail",
    "tz": "+0200",
    "auth": { "dkim": "pass", "spf": "pass", "dmarc": "pass" },
    "company": "acme.com",
    "opens": 2,
    "openedAt": 1762862500000
  },
  "notes": "VIP, prefers email",
  "groupId": "sales",
  "banned": false,
  "integrationData": { "plan": "Pro", "orders": "12" },
  "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.4 },
  "unread": 0,
  "agentUnread": 1,
  "visitorReadAt": 1762862400000,
  "triage": { "category": "billing", "urgency": "normal", "sentiment": "neutral" },
  "avatar": "https://files.pong.ee/...",
  "referrer": "https://www.google.com/",
  "landing": "https://acme.com/pricing?utm_source=newsletter",
  "lastSeen": 1762862460000,
  "lastMessageAt": 1762862400000,
  "createdAt": 1762776000000
}

Times in REST responses are Unix milliseconds. phone, fields and contactAt come from the contact form: fields holds your own form fields by the key you set for each in Settings → Contact form, and contactAt is when the visitor sent the form (null if they haven’t).

emailInfo is filled for people who wrote to your email inbox (null otherwise): the support address they wrote to, how many emails came, their mail app, the UTC offset and language of their emails, the receiving server’s DKIM / SPF / DMARC checks, their company domain (when it isn’t a free mail service) and how often your replies were opened. It is not sent in webhooks.

GET /api/clients/{id}/messages

[
  {
    "id": "8a4d1f2e9c0b7a6354e1d2c3b4a59687",
    "type": "visitor",
    "channel": "web",
    "text": "Hi! Can I change my plan mid-month?",
    "attachments": [],
    "agentId": null,
    "createdAt": 1762862400000,
    "editedAt": null
  }
]
FieldValues
typevisitor, agent, ai, system, bot (phrases of the contact form bot)
channelweb (the widget) or email (emails to your support inbox, answers to email follow-ups, and agents’ replies in those chats)
attachments[]{ kind, url, mime, size, name, width, height } — kind is image, video or file
callPresent on call cards: { state, video, sec }

PUT /api/clients/{id}

Every field is optional.

  • notes, groupId and banned are set as given (null clears).
  • phone — a string; "" or null clears it.
  • instagramId — the client’s numeric Instagram user id (the permanent one, not a username). Their direct messages to your connected Instagram account join this client’s chat, now or soon after they first write. See Verified identity.
  • fields — your contact form fields, { key: value }, merged key by key; "" or null clears a key. Keys must match [A-Za-z0-9_-]{1,40}; others are ignored.
  • Any other key — or the keys inside integrationData — is merged into the client’s custom data.
  • A custom value { "count": n } sets a counter, { "add": n } changes it by n.
curl -X PUT https://app.pong.ee/api/clients/3f9c2a1e7b8d4c60a5e2b7f1c9d03e4a \
  -H "Authorization: Pong.Simple $PONG_PUBLIC_KEY:$PONG_REST_SECRET" \
  -H "content-type: application/json" \
  -d '{ "notes": "Refund issued", "phone": "+372 5555 1234", "fields": { "company": "Acme" }, "integrationData": { "plan": "Business" } }'

Agents with the chat open see the change at once. Returns the updated client.

POST /api/messages/

curl -X POST https://app.pong.ee/api/messages/ \
  -H "Authorization: Pong.Simple $PONG_PUBLIC_KEY:$PONG_REST_SECRET" \
  -H "content-type: application/json" \
  -d '{ "clientId": "3f9c2a1e7b8d4c60a5e2b7f1c9d03e4a", "text": "Your order has shipped 📦", "agentId": "c2e5a8b1d4f70396e8a2b5c1d7f40e9b" }'
FieldRequiredWhat
clientIdyesRecipient
textyesMessage text; the widget renders links, **bold**, `code`, code blocks, > quotes and line breaks
agentIdnoShown as the author. Ignored by /api/pushedMessages/.

The visitor gets the message live if they are on the site; otherwise it waits in the widget and, when offline email replies are on, is emailed to them. Returns the message with clientId.

Errors

StatusBodyMeaning
400{ "error": "invalid json" }The body is not JSON
400{ "error": "clientId and text required" }Missing field or unknown client
401{ "error": "unauthorized" }Wrong or missing keys
404{ "error": "client not found" }No such client
429{ "error": "rate limited" }Over 120 requests per minute

Need all chats at once? Use Settings → Export in the desk: every conversation as streamed NDJSON, or one client as HTML, CSV or JSON.

← Verified identityWebhooks →