> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/api/en/preparation/detect-new-messages.md).

# Detect New Messages

To notify your system when a conversation has new messages (for example, to display notifications or alert human support agents), use Webhook delivery first; if polling is needed, read only updated co

To display a “new message” notification in your system or notify human support agents when a customer sends a message, choose an approach in this order:

1. **Use Webhook delivery first**: The conversation platform’s [message Webhook](/api/en/preparation/message-webhook.md) sends one notification per message, with no polling required (see [Use Webhooks Instead of Polling](#use-webhooks)).
2. **If you only need human handoff notifications**: Use [human handoff Webhook events](/api/en/preparation/handoff-webhook.md), which are sent only when the handoff workflow changes state.
3. **If polling is required**: Follow the [recommended polling approach](#polling) to find only updated conversations and read only the first page of each conversation’s message list.

{% hint style="warning" %}
Do not fetch the entire conversation history from newest to oldest in every polling round. A conversation with hundreds of messages requires reading several pages per round, and each page is a full query. Doing this for multiple conversations every few seconds slows down both your polling and the platform’s overall response time.
{% endhint %}

## Use Webhooks Instead of Polling <a href="#use-webhooks" id="use-webhooks"></a>

| Requirement                                                    | Recommended approach                                                                                           | When notifications are sent                                                                         |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Receive a notification for every customer message              | Use the conversation platform’s [message Webhook](/api/en/preparation/message-webhook.md) with type `Incoming` | For every customer (contact) message                                                                |
| Synchronize replies sent by AI or support agents               | Use the conversation platform’s [message Webhook](/api/en/preparation/message-webhook.md) with type `Outgoing` | For every reply sent to the customer’s conversation platform                                        |
| Receive a notification when a conversation needs a human agent | [Human handoff Webhook events](/api/en/preparation/handoff-webhook.md)                                         | Only for human handoff, agent assignment, return to AI, or queue timeout; **not** for every message |

Configuration path: Admin console → <mark style="color:blue;">Customer Conversations</mark> → <mark style="color:blue;">Conversation Platforms</mark> → select a conversation platform → <mark style="color:blue;">Webhook Settings</mark> → <mark style="color:blue;">Add Webhook</mark>. Set <mark style="color:blue;">Type</mark> to <mark style="color:blue;">Incoming</mark> or <mark style="color:blue;">Outgoing</mark> (although the admin console describes <mark style="color:blue;">Incoming</mark> as “Receive external requests,” it actually sends each customer message to your configured URL; <mark style="color:blue;">Receive</mark> is the type that receives external messages). Set <mark style="color:blue;">Payload Format</mark> to <mark style="color:blue;">MaiAgent Standard</mark> (MaiAgent’s standard format). See [Message Webhook](/api/en/preparation/message-webhook.md) for payload contents, source verification, and retry behavior.

The WhatsApp conversation platform’s settings page currently has no <mark style="color:blue;">Webhook Settings</mark> tab. Use the polling approach below instead.

The two Webhook types are configured independently and can be enabled together; deduplicate their payloads using the message or event `id`. Message Webhooks automatically retry up to 3 times on timeouts, connection failures, `5xx`, or `429` responses (the same message may arrive more than once). If all 3 retries fail, delivery stops; delivery is not guaranteed. The polling approach below can check for missed **customer messages** (Incoming) at a low frequency. If delivery of an AI or support agent reply (Outgoing) fails, the polling example may not retrieve it. Check the Webhook delivery logs for failed deliveries instead (see [Message Webhook → Responses and Retries](/api/en/preparation/message-webhook.md#response-and-retry)).

## Recommended Polling Approach <a href="#polling" id="polling"></a>

If you cannot receive Webhooks (for example, because the receiving endpoint cannot be exposed publicly), poll using the following two APIs together.

### 1. Find Updated Conversations <a href="#find-changed-conversations" id="find-changed-conversations"></a>

```
GET /api/v1/conversations/?inbox=<inbox id>&page_size=20
```

The conversation list is always sorted **by last message time, newest first** (`lastMessageCreatedAt` in the response). For each conversation, record the last observed `lastMessageCreatedAt` and the last message’s `lastMessage.id`. A change to either means the conversation has been updated (timestamps have second precision, so if two messages arrive in the same second, only the `id` changes). In each polling round, read from the first entry down and stop when you reach a conversation older than “the latest time observed so far minus 10 minutes.” Usually, you only need the first page. Follow the response’s `next` to the next page only if more than `page_size` conversations were updated within that window.

The extra 10-minute lookback is necessary because, in API conversations, a conversation’s `lastMessageCreatedAt` may be written only after the AI reply finishes generating. The value is still the customer message’s timestamp, so it may be older than other conversations you saw in the previous round. Comparing only whether it is newer than the previous round’s latest time would miss these conversations.

### 2. Read Only the First Page of Messages for Each Updated Conversation <a href="#read-latest-page" id="read-latest-page"></a>

```
GET /api/v1/messages/?conversation=<conversation id>&page_size=20
```

The **first page of the message list contains the latest messages**: the system selects the latest `page_size` messages, then sorts them from oldest to newest within the page. The last message is therefore the latest in the entire conversation. The response’s `next` points to **older** messages; do not read back through the history in every round when detecting new messages.

Use each message’s `id` to determine whether it has been processed: unprocessed `id` values on the first page identify new messages. Pagination is needed in only one case—when **all** messages on the first page are unprocessed (more than `page_size` messages arrived between rounds). Only then follow `next` to older pages until you encounter a processed message.

### 3. Polling Interval and Page Size <a href="#interval-and-page-size" id="interval-and-page-size"></a>

| Setting          | Recommendation | Description                                                                                 |
| ---------------- | -------------- | ------------------------------------------------------------------------------------------- |
| Polling interval | 30–60 seconds  | Use Webhooks for more immediate notifications; do not shorten the interval to a few seconds |
| `page_size`      | 20             | Default 20, maximum 100. Larger pages produce slower responses and more content             |

{% hint style="info" %}

* Response fields use camelCase. `lastMessageCreatedAt` and `createdAt` are timestamp strings in milliseconds (for example, `"1790753081000"`), but have only second precision. Multiple messages may arrive within the same second; deduplicate by message `id`, rather than comparing timestamps alone.
* The example uses the local time as the starting point for detection. Enable network time synchronization (NTP) on the host to prevent a fast clock from skipping messages at startup.
* The conversation list includes only conversations with customer messages. Conversations containing only an opening greeting, where the customer has not yet spoken, do not appear.
* `lastMessageCreatedAt` always updates when a new customer message arrives, but the update may occur after the message itself is written (see step 1 above). An automatic AI reply **does not always** update it. This polling approach therefore detects only customer messages: an AI reply is retrieved along with them only if it has already been generated at the time of reading. If an AI reply is generated after the customer message has been read and does not change the conversation’s last message time or last message, subsequent polling skips that conversation and **will not** retrieve the reply. If generating a single AI reply takes more than 10 minutes (for example, due to a long-running tool), detection of the customer message may also be delayed until the conversation is updated again. To receive every customer message and reply promptly, use <mark style="color:blue;">Incoming</mark>/<mark style="color:blue;">Outgoing</mark> message Webhooks (temporary failures are retried up to 3 times, but delivery is still not guaranteed).
  {% endhint %}

### Request Examples <a href="#examples" id="examples"></a>

{% tabs %}
{% tab title="cURL" %}

```bash
# 1. List the conversation platform’s conversations by last message time, newest first
curl "https://api.maiagent.ai/api/v1/conversations/?inbox=YOUR_INBOX_ID&page_size=20" \
  -H "Authorization: Api-Key YOUR_API_KEY"

# 2. Read only the first page of messages for updated conversations (latest 20, oldest first within the page)
curl "https://api.maiagent.ai/api/v1/messages/?conversation=CONVERSATION_ID&page_size=20" \
  -H "Authorization: Api-Key YOUR_API_KEY"
```

{% endtab %}

{% tab title="Python" %}

```python
import os
import time

import requests

API = "https://api.maiagent.ai/api/v1"
HEADERS = {"Authorization": f"Api-Key {os.environ['MAIAGENT_API_KEY']}"}
INBOX_ID = os.environ["INBOX_ID"]
POLL_SECONDS = 30
PAGE_SIZE = 20
LOOKBACK_MS = 10 * 60 * 1000  # Look back an extra 10 minutes: the conversation’s last message time may be written only after the AI reply is generated


def get(url: str, params: dict | None = None) -> dict:
    resp = requests.get(url, headers=HEADERS, params=params, timeout=10)
    resp.raise_for_status()
    return resp.json()


def recent_conversations(since_ms: int) -> list[dict]:
    """Conversations are sorted by last message time, newest first: stop at entries older than since_ms; usually only the first page is needed."""
    conversations = []
    url, params = f"{API}/conversations/", {"inbox": INBOX_ID, "page_size": PAGE_SIZE}
    while url:
        page = get(url, params)
        for conv in page["results"]:
            if int(conv["lastMessageCreatedAt"]) < since_ms:
                return conversations
            conversations.append(conv)
        url, params = page["next"], None
    return conversations


def new_messages(conversation_id: str, seen_ids: set[str], not_before_ms: int) -> list[dict]:
    """Read the first page (latest messages, oldest first within the page); read older pages only if the entire page is unprocessed."""
    found = []
    url, params = f"{API}/messages/", {"conversation": conversation_id, "page_size": PAGE_SIZE}
    while url:
        page = get(url, params)
        fresh = [m for m in page["results"] if m["id"] not in seen_ids and int(m["createdAt"]) >= not_before_ms]
        found = fresh + found
        if len(fresh) < len(page["results"]):
            break  # This page contains a processed message (or one from before detection started); no need to read further back
        url, params = page["next"], None
    return found


def change_key(conv: dict) -> tuple:
    """Timestamps have second precision: for two messages in the same second, only the last message id changes, so compare both."""
    return conv["lastMessageCreatedAt"], (conv.get("lastMessage") or {}).get("id")


def main() -> None:
    started_ms = int(time.time()) * 1000  # Start detecting now (rounded down to the second), without processing historical messages
    newest_ms = started_ms
    last_seen: dict[str, tuple] = {}  # Last observed change_key for each conversation (persist or periodically clean up in production)
    seen_ids: dict[str, set[str]] = {}  # Processed message ids for each conversation (same as above)
    while True:
        try:
            round_newest = newest_ms
            for conv in recent_conversations(newest_ms - LOOKBACK_MS):
                if last_seen.get(conv["id"]) == change_key(conv):
                    continue  # No new customer messages in this conversation (AI replies generated later will not be read here)
                ids = seen_ids.setdefault(conv["id"], set())
                for msg in new_messages(conv["id"], ids, started_ms):
                    print(f"[New message] Conversation {conv['id']} {msg['type']}: {msg['content'][:40]}")
                    ids.add(msg["id"])
                last_seen[conv["id"]] = change_key(conv)
                round_newest = max(round_newest, int(conv["lastMessageCreatedAt"]))
            newest_ms = round_newest  # Advance only after the entire polling round succeeds
        except requests.RequestException as exc:
            print(f"Query failed this round; retrying next round: {exc}")  # The lookback starting point has not advanced; unfinished conversations will be picked up next round
        time.sleep(POLL_SECONDS)


if __name__ == "__main__":
    main()
```

{% endtab %}
{% endtabs %}

An API key has the same permissions as its owner’s role in the organization. The owner must be able to view conversations on the relevant conversation platform. See [API Authentication → API Key Permission Scope](/api/en/preparation/authentication.md#api-key-permissions).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.maiagent.ai/api/en/preparation/detect-new-messages.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
