# Live Chat

The Live Chat system enables agent-based customer conversations with ticket tracking, team assignment, and message history across WhatsApp, Web, and Facebook channels.

Authentication Required
All API requests require your Developer Key in the `Authorization` header. No `Bearer` prefix — pass the key directly.
See [Authentication](/guides/authentication) for setup and security best practices.

## Architecture

The Live Chat system is built around three core concepts:

| Concept | Description |
|  --- | --- |
| **Chat Participants** | Active conversations between a customer and your platform |
| **Tickets** | Track and manage conversations with status workflows |
| **Agents & Teams** | Support staff organized with team assignment and supervisor oversight |


### Conversation flow

```
Customer Message ──> Unassigned Queue ──> Agent Picks Up ──> Active Chat
                                                                  │
                                                    ┌─────────────┼─────────────┐
                                                    ▼             ▼             ▼
                                                 Resolved    Transferred     Closed
                                                  (Ticket)   (to team)    (Rating widget)
```

## Chatbot conversation flow

Follow this three-step flow to receive an incoming WhatsApp message and retrieve the full conversation history.

```
Incoming Message ──> Webhook fires ──> Look up ChatParticipant ──> Get messages
    (customer)       (your server)      (by sender number)        (by participant ID)
```

### Step 1: Receive the incoming message webhook

When a customer sends a message, your configured `incomingMessageURL` receives a POST with the message payload. See [Webhooks — Incoming message payload](/guides/webhooks#incoming-message-payload) for full details.

```json
{
  "profileName": "John Smith",
  "sender": "+27987654321",
  "senderUserId": "ZA.1021033720668862",
  "username": "john_smith",
  "receiver": "+27123456789",
  "text": "Hi, I need help with my order",
  "messageType": "INTERACTION",
  "messageContentType": "TEXT",
  "gstId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "participantId": "participant-uuid",
  "sentTime": "2026-01-15T10:30:00.000+00:00",
  "status": "RECEIVED"
}
```

Alpha usernames (BSUID)
WhatsApp is rolling out **alpha usernames**. Each customer now has a durable, business-scoped user id (**BSUID**, e.g. `ZA.1021033720668862`) that is stable for your business even if the customer changes their phone number — and that stays present on inbound events **even when the phone number is withheld**. Customers may also set an optional **username** (display only).

Incoming and chatbot webhooks carry these as `senderUserId` / `recipientUserId` (the BSUID) and `username`. **The phone number (`sender` / `senderMsisdn`) remains the primary identifier today** — the BSUID is an additional, more durable key you can store now and rely on as phone numbers become optional. All three fields are omitted when the provider doesn't supply them.

Use the `sender` field without a `+` to look up the chat participant in Step 2. When the phone number is withheld, match on `userId` (the customer BSUID) instead — it is present on the chat participant even when the number is not.

`participantId` is not the live-chat participant id
The `participantId` field on the incoming-message webhook payload identifies a participant in the conversation engine. It is **not** the same as the live-chat `chatParticipantId` used to fetch messages or send agent replies. Always resolve the live-chat participant by `sender` (Step 2) before calling any `/pl4chat-service/api/v2/messages` or `/pl4chat-service/api/v2/chats` endpoints.

### Step 2: Get the chat participant by sender number

List your chats and filter by the sender's phone number to find the latest conversation. The v2 list
endpoint selects which conversations to return with a `scope` filter — the agent and client are
resolved from your API key, so you only choose the tab:

```
GET /pl4chat-service/api/v2/chats?scope=my
```

#### Choose your scope

| `scope` | Returns |
|  --- | --- |
| `my` | Conversations assigned to you (the authenticated agent) |
| `team` | Conversations assigned to your team members |
| `unassigned` | Conversations waiting in queue |
| `closed` | Closed conversations |
| `missed-agent` | Missed conversations |
| `missed-after-hours` | After-hours missed conversations |
| `my-missed` | Your missed conversations |
| `open` | All open client conversations |
| `all` | All conversations (open and closed) |


| Parameter | Type | Description |
|  --- | --- | --- |
| `scope` | string | Which list to return (see above). **Required** |
| `page` | integer | Page number (0-based). Default: `0` |
| `size` | integer | Page size. Default: `10` |
| `search` | string | Filter by phone number, name, or customer id |


Use the `search` parameter with the sender's phone number or BSUID (`senderUserId`) to narrow
results directly, or filter client-side after fetching. The response is a page of
`ChatParticipantResponse` objects. Match on `phoneNumber` **or** `userId` — a customer with a
withheld phone number is identified only by `userId` (BSUID).

cURL
```bash
curl -X GET \
  "https://api.payless4messaging.com/pl4chat-service/api/v2/chats?scope=unassigned&page=0&size=50&search=27987654321" \
  -H "Authorization: $ECOMM_API_KEY"
```

Node.js
```javascript
const API_KEY = process.env.ECOMM_API_KEY;
const BASE_URL = 'https://api.payless4messaging.com';

// The sender number from the webhook payload
const senderNumber = '27987654321';

const response = await fetch(
  `${BASE_URL}/pl4chat-service/api/v2/chats?scope=unassigned&page=0&size=50&search=${encodeURIComponent(senderNumber)}`,
  { headers: { 'Authorization': API_KEY } }
);

const data = await response.json();

// Match on phone number OR userId (BSUID) — the customer may have no phone number
const participant = data.content
  .filter(p => p.phoneNumber === senderNumber || p.userId === senderNumber)
  .sort((a, b) => new Date(b.lastMessageTime) - new Date(a.lastMessageTime))[0];

if (!participant) {
  throw new Error(`No chat participant found for ${senderNumber}`);
}

console.log(`Participant ID: ${participant.id}`);
```

Python
```python
import os
import requests

API_KEY = os.environ["ECOMM_API_KEY"]
BASE_URL = "https://api.payless4messaging.com"

# The sender number from the webhook payload
sender_number = "27987654321"

response = requests.get(
    f"{BASE_URL}/pl4chat-service/api/v2/chats",
    headers={"Authorization": API_KEY},
    params={"scope": "unassigned", "page": 0, "size": 50, "search": sender_number},
)

data = response.json()

# Match on phone number OR userId (BSUID) — the customer may have no phone number
participants = [
    p for p in data["content"]
    if p.get("phoneNumber") == sender_number or p.get("userId") == sender_number
]
if not participants:
    raise ValueError(f"No chat participant found for {sender_number}")
participant = sorted(participants, key=lambda p: p["lastMessageTime"], reverse=True)[0]

print(f"Participant ID: {participant['id']}")
```

Java
```java
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import com.fasterxml.jackson.databind.*;

String API_KEY = System.getenv("ECOMM_API_KEY");
String BASE_URL = "https://api.payless4messaging.com";
String senderNumber = "27987654321";
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(BASE_URL + "/pl4chat-service/api/v2/chats?scope=unassigned&page=0&size=50&search="
        + URLEncoder.encode(senderNumber, StandardCharsets.UTF_8)))
    .header("Authorization", API_KEY)
    .GET()
    .build();

HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode data = mapper.readTree(response.body());

// Match on phone number OR userId (BSUID) — the customer may have no phone number
String participantId = null;
String latestTime = "";
for (JsonNode p : data.get("content")) {
    String phone = p.has("phoneNumber") ? p.get("phoneNumber").asText() : "";
    String userId = p.has("userId") ? p.get("userId").asText() : "";
    if (phone.equals(senderNumber) || userId.equals(senderNumber)) {
        String msgTime = p.get("lastMessageTime").asText();
        if (msgTime.compareTo(latestTime) > 0) {
            latestTime = msgTime;
            participantId = p.get("id").asText();
        }
    }
}
System.out.println("Participant ID: " + participantId);
```

### Step 3: Get the conversation messages

Use the participant ID from step 2 to retrieve the full message history:

cURL
```bash
curl -X GET \
  "https://api.payless4messaging.com/pl4chat-service/api/v2/messages?participantId=participant-uuid" \
  -H "Authorization: $ECOMM_API_KEY"
```

Node.js
```javascript
const messages = await fetch(
  `${BASE_URL}/pl4chat-service/api/v2/messages?participantId=${participant.id}`,
  { headers: { 'Authorization': API_KEY } }
).then(r => r.json());

messages.forEach(msg => {
  const who = msg.direction === 'INCOMING' ? 'Customer' : 'Bot';
  console.log(`[${msg.createdTime}] ${who}: ${msg.text}`);
});
```

Python
```python
response = requests.get(
    f"{BASE_URL}/pl4chat-service/api/v2/messages",
    headers={"Authorization": API_KEY},
    params={"participantId": participant["id"]},
)

for msg in response.json():
    who = "Customer" if msg["direction"] == "INCOMING" else "Bot"
    print(f"[{msg['createdTime']}] {who}: {msg['text']}")
```

Java
```java
HttpRequest msgRequest = HttpRequest.newBuilder()
    .uri(URI.create(BASE_URL + "/pl4chat-service/api/v2/messages?participantId=" + participantId))
    .header("Authorization", API_KEY)
    .GET()
    .build();

HttpResponse<String> msgResponse = client.send(msgRequest, BodyHandlers.ofString());
JsonNode messages = mapper.readTree(msgResponse.body());

for (JsonNode msg : messages) {
    String who = "INCOMING".equals(msg.get("direction").asText()) ? "Customer" : "Bot";
    System.out.printf("[%s] %s: %s%n",
        msg.get("createdTime").asText(), who, msg.get("text").asText());
}
```

### Alternative: Get messages by tracking number

If you have the **GSTN** (Global System Tracking Number) or **CTN** (Customer Tracking Number) from the chat participant object, you can retrieve messages directly without needing the participant ID:

```
GET /pl4chat-service/api/v2/messages/tracking/{trackingNumber}
```

cURL
```bash
curl -X GET \
  "https://api.payless4messaging.com/pl4chat-service/api/v2/messages/tracking/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890" \
  -H "Authorization: $ECOMM_API_KEY"
```

Node.js
```javascript
const gstn = 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'; // from chat participant

const messages = await fetch(
  `${BASE_URL}/pl4chat-service/api/v2/messages/tracking/${gstn}`,
  { headers: { 'Authorization': API_KEY } }
).then(r => r.json());
```

Python
```python
gstn = "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890"  # from chat participant

response = requests.get(
    f"{BASE_URL}/pl4chat-service/api/v2/messages/tracking/{gstn}",
    headers={"Authorization": API_KEY},
)
```

Java
```java
String gstn = "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890"; // from chat participant

HttpRequest trackingRequest = HttpRequest.newBuilder()
    .uri(URI.create(BASE_URL + "/pl4chat-service/api/v2/messages/tracking/" + gstn))
    .header("Authorization", API_KEY)
    .GET()
    .build();

HttpResponse<String> trackingResponse = client.send(trackingRequest, BodyHandlers.ofString());
JsonNode trackingMessages = mapper.readTree(trackingResponse.body());
```

### Mark a conversation as read

After retrieving messages, mark the conversation as read — this clears the unread badge and sends a read receipt to the customer in one call:

```
POST /pl4chat-service/api/v2/messages/read?participantId=cp-a1b2c3d4
```

## Chat participants

Each chat participant carries:

| Field | Type | Description |
|  --- | --- | --- |
| `id` | string | Chat participant id |
| `phoneNumber` | string | Customer phone number. May be absent when the number is withheld |
| `userId` | string | Customer **BSUID** (e.g. `ZA.1021033720668862`), durable across phone-number changes. Present even when `phoneNumber` is not |
| `username` | string | Customer's optional WhatsApp username (display only) |
| `name` | string | Display name |


### Terminate or reopen a chat

Close or reopen a conversation with its id — the acting agent is resolved from your API key, so there's no request body to assemble:

```
POST /pl4chat-service/api/v2/chats/{id}/terminate   # close (sends the rating widget if configured)
POST /pl4chat-service/api/v2/chats/{id}/open        # reopen and assign to you
```

Both return the updated `ChatParticipantResponse`.

The assigned-agent slot is freed for you
Terminating a chat moves its current `assignedTo` to `lastAssignedAgent` and frees the slot in the agent's chat-limit count automatically. (The deprecated `PUT /pl4chat-service/ChatParticipant/terminateChatParticipantChat` required you to echo the full object back to make that happen — v2 does it server-side.)

## Agent messaging

Why this endpoint, not the notifications one?
Only messages sent through the live-chat send endpoint show up in the live-chat UI. Outbound sends through the notifications endpoint (`/whatsapp-api-service/v2/channels/whatsapp`) reach the customer but bypass the chat service entirely — agents won't see them. See [How the system fits together](/guides/getting-started#how-the-system-fits-together) for the architecture.

### Send a reply

One endpoint sends on every channel. Set `channel` (`WHATSAPP`, `WEB`, or `FACEBOOK`) and `text` — the agent is resolved from your API key:

```
POST /pl4chat-service/api/v2/messages
```

```json
{
  "participantId": "participant-uuid",
  "channel": "WHATSAPP",
  "tempId": "unique-temp-uuid",
  "text": "Here is the information you requested."
}
```

`tempId` is optional — supply a client-generated string only if you want it echoed back to reconcile an optimistic UI message; otherwise omit it. The response is the stored `MessageResponse` (with your `tempId` echoed back when you sent one). To quote/reply to an earlier message, also set `quotedExternalMessageId`.

### Send media

Upload the file, then send the returned `mediaUrl` — the same flow works for any channel:

```
POST /pl4chat-service/api/v2/messages/media   # multipart/form-data, field "file" -> { mediaUrl, mediaType }
POST /pl4chat-service/api/v2/messages         # { participantId, channel, mediaUrl, text? }
```

The content type defaults to `MEDIA` when a `mediaUrl` is present, so you don't need to set it.

### Typing indicator

```
POST /pl4chat-service/api/v2/messages/typing?participantId=participant-uuid
```

Shows a typing bubble to the customer while the agent is composing a reply.

### Mark a conversation as read

```
POST /pl4chat-service/api/v2/messages/read?participantId=participant-uuid
```

## Tickets

### Search tickets

```
GET /pl4chat-service/ticket/search
```

**Required parameters:**

| Parameter | Type | Description |
|  --- | --- | --- |
| `page` | integer | Page number (0-based) |
| `size` | integer | Page size |
| `startDateTime` | string | Start of date range |
| `endDateTime` | string | End of date range |
| `clientRef` | string | Client reference |


**Optional filters:**

| Filter | Type | Description |
|  --- | --- | --- |
| `whatsAppNumber` | string | Customer WhatsApp number |
| `rating` | enum | `poor`, `average`, or `excellent` |
| `ticketNumber` | string | Specific ticket number (e.g., `TCK-001`) |
| `agentId` | string | Filter by assigned agent |
| `selectedStatus` | enum | `open`, `closed`, `awaiting agent`, `awaiting customer`, `resolved` |


### Update ticket status

```
PUT /pl4chat-service/ticket/{ticketId}/status?newStatus=RESOLVED
```

**Available statuses:**

| Status | Description |
|  --- | --- |
| `OPEN` | Ticket is active and being worked on |
| `AWAITING_AGENT` | Waiting for an agent to respond |
| `AWAITING_CUSTOMER` | Waiting for the customer to respond |
| `RESOLVED` | Issue has been resolved but ticket remains accessible |
| `CLOSED` | Ticket is closed and archived |


### Get ticket by number

```
GET /pl4chat-service/ticket/by-ticket-number?ticketNumber=TCK-001
```

## Contacts

Manage customer contact records linked to your account.

| Method | Endpoint | Description |
|  --- | --- | --- |
| `GET` | `/payless4messaging-service/WhatsApp/contacts` | Get all contacts (paginated) |
| `POST` | `/payless4messaging-service/WhatsApp/contacts` | Create a contact |
| `PUT` | `/payless4messaging-service/WhatsApp/contacts/{id}` | Update a contact |
| `DELETE` | `/payless4messaging-service/WhatsApp/contacts/{id}` | Delete a contact |


Limited update fields
Currently, only the `fullName` field can be updated on an existing contact. Other fields (phone number, business name, etc.) are read-only after creation.

```json
PUT /payless4messaging-service/WhatsApp/contacts/{id}

{
  "fullName": "Jane Smith"
}
```

## Supported channels

| Channel | Value | Description |
|  --- | --- | --- |
| WhatsApp | `WHATSAPP` | WhatsApp conversations via Cloud or Lite |
| Web | `WEB` | Web chat widget embedded in your website |
| Facebook | `FACEBOOK` | Facebook Messenger conversations |


## Message content types

| Type | Description |
|  --- | --- |
| `TEXT` | Plain text message |
| `IMAGE` | Image attachment (JPG, PNG, GIF) |
| `DOCUMENT` | Document attachment (PDF, DOCX, XLSX) |
| `VIDEO` | Video attachment (MP4) |
| `AUDIO` | Audio message |
| `VOICE` | Voice note |
| `FILE` | Generic file attachment |
| `LOCATION` | Location with latitude/longitude |
| `BUTTON` | Button interaction response |
| `CAROUSEL` | Carousel card |
| `RATING` | Customer rating response |