---
title: "Search archived chat messages by text content"
method: GET
path: "/v2/apps/{appId}/messages/search"
tags: ["Chats"]
---

# Search archived chat messages by text content

`GET /v2/apps/{appId}/messages/search`

## Request

- Base URL: `https://api.chat.ethora.com`
- URL: `https://api.chat.ethora.com/v2/apps/{appId}/messages/search`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

Searches the Mongo archive (`*_chat_archive.messages`) populated by the
ejabberd `mod_track_message` hook. This is a parallel store to the live
MAM history — denormalized, indexed for `$text` + substring queries, and
isolated from the live XMPP DB so search load never touches the chat
runtime.

Two query modes:
- `substring` (default): case-insensitive regex on `body`. Finds short
  queries like `"h"` inside `"hello"` — good for autocomplete-style
  flows. Special regex characters are escaped, so a query like `"."`
  matches a literal dot, not "any char".
- `fulltext`: Mongo `$text` operator. Token-based; won't match
  substrings of a word, but very fast and supports `sort=relevance`
  with a `score` field on each item.

Visibility:
- **B2B server token**: full access to every message in the target app.
- **User token**: restricted to chats the caller can see. Groupchat
  membership is resolved via `user_to_chats`; private 1-1 access is
  derived from the canonical chatId (the caller's xmppUsername must be
  one of the two participants).

When the caller passes an explicit `chatId` they have no access to,
the response is `403 CHAT_FORBIDDEN` (not an empty list) so the
client can distinguish "denied" from "no matches".

Note: the archive only stores messages whose chat / users are
registered in the Ethora API. Ad-hoc XMPP rooms and synthetic test
fixtures don't pollute the searchable set.

## Path parameters

- `appId` string, required

## Query parameters

- `q` string, required
- `mode` 'substring' | 'fulltext'
- `chatId` string
- `fromUserId` string
- `since` string, date-time
- `until` string, date-time
- `sort` 'date' | 'relevance'
- `limit` integer
- `offset` integer

## Response `200`

```
{
  items: [
    {
      _id, appId, chatId, chatType, room,
      from, fromUserId, to, toUserId,
      body, messageId, stanzaId, createdAt,
      score?    // only when mode=fulltext AND sort=relevance
    }
  ],
  total: number,
  limit, offset, q, mode
}
```

## Other responses

- `400`: Bad Request
- `401`: Unauthorized
- `403`: Caller has no access to the requested chat (only when `chatId` is provided). ``` { error: string, code: 'CHAT_FORBIDDEN', details: { chatId } } ```
- `404`: Not Found
- `422`: Validation error (missing `q`, `q` too long, invalid mode/sort, limit > 100, etc.). ``` { error: string, code: 'VALIDATION_ERROR' } ```
- `500`: Internal Server Error

---

[API](https://skmtc.dev/ethora/apis/platform-api.md) · [All operations](https://skmtc.dev/ethora/apis/platform-api/llms.txt) · [OpenAPI document](https://skmtc.dev/ethora/apis/platform-api/revisions/01b6e64378ee?raw)
