REST API
Read clients and their messages, update client data and post messages from your backend. JSON in, JSON out.
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
| Method | Path | What |
|---|---|---|
GET | /api/clients/{id} | One client |
GET | /api/clients/{id}/messages | The 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
}
]
| Field | Values |
|---|---|
type | visitor, agent, ai, system, bot (phrases of the contact form bot) |
channel | web (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 |
call | Present on call cards: { state, video, sec } |
PUT /api/clients/{id}
Every field is optional.
notes,groupIdandbannedare set as given (nullclears).phone— a string;""ornullclears 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;""ornullclears 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 byn.
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" }'
| Field | Required | What |
|---|---|---|
clientId | yes | Recipient |
text | yes | Message text; the widget renders links, **bold**, `code`, code blocks, > quotes and line breaks |
agentId | no | Shown 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
| Status | Body | Meaning |
|---|---|---|
| 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.