> ## Documentation Index
> Fetch the complete documentation index at: https://docs.masivo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ticket management

> Complete guide to managing tickets, assignees, stages, and multi-channel communication threads via the dashboard and Storefront API.

This guide is written for **support leads, CRM operators, and integrators** who need to run tickets correctly in Masivo — including when conversations happen on **email, WhatsApp, chat, and reviews** at the same time.

If terms like *thread* or *external\_message\_id* feel unfamiliar, read [Tickets & communication (concepts)](/concepts/tickets) first, then come back here for practical steps.

## Before you start

<Steps>
  <Step title="Get your credentials">
    You need a **SERVER API key** and your **account id** (`x-account-id`). Create keys in the dashboard under **Settings → API keys**. Never expose SERVER keys in browser code.
  </Step>

  <Step title="Know the base URL">
    All examples use:

    ```
    https://app.masivo.ai/api/storefront/v1
    ```

    Every request also requires:

    ```http theme={null}
    Authorization: Bearer <SERVER_API_KEY>
    x-account-id: <ACCOUNT_UUID>
    Content-Type: application/json
    ```
  </Step>

  <Step title="Pick your workflow">
    * **Dashboard only:** use CRM → Tickets for day-to-day work.
    * **Integration:** your mail/helpdesk connector creates threads + messages via API; agents work in Masivo or your UI.
    * **Hybrid:** reviews create tickets automatically; agents reply via API or dashboard.
  </Step>
</Steps>

***

## Quick reference — all ticket & communication endpoints

| Method   | Path                                   | What it does                                                            |
| -------- | -------------------------------------- | ----------------------------------------------------------------------- |
| `GET`    | `/ticket-pipelines`                    | List pipelines and ordered stages, including internal open/closed state |
| `POST`   | `/tickets`                             | Create a ticket (manual, from review, with optional assignees)          |
| `GET`    | `/tickets/{id}`                        | Get one ticket **including** `communication_summary` and assignees      |
| `PATCH`  | `/tickets/{id}`                        | Update title, priority, stage, assignees in one call                    |
| `GET`    | `/tickets/{id}/assignees`              | List active assignee records                                            |
| `POST`   | `/tickets/{id}/assignees`              | Add one assignee or watcher                                             |
| `DELETE` | `/tickets/{id}/assignees`              | Remove an assignee                                                      |
| `GET`    | `/communication/threads`               | List thread **shells** (summaries, no message bodies)                   |
| `POST`   | `/communication/threads`               | Attach a new thread to a ticket (or review/customer)                    |
| `GET`    | `/communication/threads/{id}`          | Get one thread shell                                                    |
| `GET`    | `/communication/threads/{id}/messages` | List messages in chronological order                                    |
| `POST`   | `/communication/threads/{id}/messages` | Add inbound, outbound, or internal message                              |

API reference pages with schemas: [Tickets group](/api-reference/tickets/post) in the sidebar.

***

## Part 1 — Creating tickets

### 1A. Find the pipeline and stage ids

```bash theme={null}
curl "https://app.masivo.ai/api/storefront/v1/ticket-pipelines" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID"
```

Use the returned pipeline `id` and stage `code`. If both are omitted when creating a ticket, Masivo uses the account's default pipeline and that pipeline's default stage.

Pipeline administration is dashboard-only under **CRM → Pipelines**. The
Storefront API intentionally exposes discovery through `GET /ticket-pipelines`
but does not expose create, update, or delete pipeline endpoints.

### 1B. Create a manual ticket

Use when an agent or your system opens a case without a linked review.

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/tickets" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Order #8821 arrived damaged",
    "description": "Customer sent photos of broken packaging.",
    "pipeline_id": "770e8400-e29b-41d4-a716-446655440002",
    "stage_code": "new",
    "priority": "high",
    "source_type": "manual",
    "customer_id": "cust_external_123",
    "assignee_user_ids": ["550e8400-e29b-41d4-a716-446655440000"],
    "idempotency_key": "create-ticket-order-8821-v1"
  }'
```

| Field               | Required? | Notes                                                                         |
| ------------------- | --------- | ----------------------------------------------------------------------------- |
| `title`             | Yes       | Max 200 characters                                                            |
| `description`       | No        | Max 5000 characters                                                           |
| `pipeline_id`       | No        | Pipeline UUID from `GET /ticket-pipelines`; defaults to the account pipeline  |
| `stage_code`        | No        | Internal stage code in that pipeline; defaults to its default stage           |
| `priority`          | No        | `low`, `medium`, `high`, `urgent` — default `medium`                          |
| `source_type`       | No        | Default `manual`                                                              |
| `review_id`         | No        | If set, creates a **review** ticket instead (see below)                       |
| `customer_id`       | No        | External customer id; stored as a `customer` association                      |
| `assignee_user_ids` | No        | Array of team member UUIDs                                                    |
| `actor_user_id`     | No        | Who performed the action; defaults to a system actor                          |
| `idempotency_key`   | No        | **Strongly recommended.** Re-posting the same key returns the original ticket |
| `required_fields`   | No        | Required when creating directly in `resolved` / `closed` stage                |

**Tip:** Always send an `idempotency_key` from your integration so network retries do not create duplicate tickets.

### 1C. Create a ticket from a review

When `review_id` is present, Masivo:

1. Creates a ticket with `source_type: review`
2. Deduplicates — only **one open ticket per review**
3. Links review, customer, and journey associations when available
4. Creates a **review communication thread** with the review answers as the first inbound message

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/tickets" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "review_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Negative review — delivery issue",
    "priority": "urgent",
    "idempotency_key": "review-ticket-a1b2c3d4"
  }'
```

After creation, open the ticket in the dashboard **Conversation** tab — you should see a `review` channel thread with the submission text.

***

## Part 2 — Reading and updating tickets

### 2A. Get a ticket (with communication rollup)

```bash theme={null}
curl "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID"
```

Example response (abbreviated — important fields only):

```json theme={null}
{
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "pipeline_id": "770e8400-e29b-41d4-a716-446655440002",
    "stage_code": "in_progress",
    "status": "open",
    "priority": "high",
    "title": "Order #8821 arrived damaged",
    "description": "Customer sent photos of broken packaging.",
    "source_type": "manual",
    "review_id": null,
    "brand_id": "brand_01",
    "channel_id": "web",
    "store_id": "store_nyc",
    "assignees": [
      {
        "user_id": "660e8400-e29b-41d4-a716-446655440001",
        "role": "assignee",
        "assigned_at": "2026-06-25T14:00:00.000Z",
        "assigned_by_user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "communication_summary": {
      "thread_count": 2,
      "latest_thread_id": "8b3b9f2a-1c2d-4e5f-9a0b-1c2d3e4f5a6b",
      "latest_message_id": "9c4c0a3b-2d3e-4f60-0b1c-2d3e4f5a6b7c",
      "latest_message_at": "2026-06-25T16:30:00.000Z",
      "latest_message_preview": "Thanks, replacement is on the way.",
      "channel": "whatsapp",
      "channel_account_id": "+5491112345678",
      "external_thread_id": "wa-thread-8821",
      "direction": "outbound",
      "unread_count": 3,
      "status": "open"
    },
    "associations": [
      {
        "associated_type": "customer",
        "associated_id": "cust_external_123",
        "created_at": "2026-06-25T14:00:00.000Z"
      }
    ],
    "stage_history": [],
    "metadata": {},
    "resolution_summary": null,
    "closure_reason": null,
    "created_at": "2026-06-25T14:00:00.000Z",
    "updated_at": "2026-06-25T16:30:00.000Z"
  }
}
```

Use `communication_summary` on list/board views. Load full threads only when the user opens the conversation.

### 2B. Update title, description, or priority

```bash theme={null}
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Order #8821 — replacement shipped",
    "priority": "medium"
  }'
```

### 2C. Change stage (move on the board)

```bash theme={null}
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "to_stage_code": "waiting_customer"
  }'
```

Valid values are scoped to the ticket's pipeline. Read them from `GET /ticket-pipelines`; do not hard-code display names.

To move between pipelines, send both target values:

```json theme={null}
{
  "to_pipeline_id": "880e8400-e29b-41d4-a716-446655440003",
  "to_stage_code": "a2ac1542-934c-4b8f-b8cf-4a64ba923fb5"
}
```

**Resolving** — you must include a resolution summary:

```bash theme={null}
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "to_stage_code": "resolved",
    "required_fields": {
      "resolution_summary": "Replacement order shipped via WhatsApp confirmation."
    }
  }'
```

**Closing** — you must include a closure reason:

```bash theme={null}
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "to_stage_code": "closed",
    "required_fields": {
      "closure_reason": "Customer confirmed receipt. No further action."
    }
  }'
```

**Reopening:** move back to any open stage. Masivo records `reopened_at` when leaving a closed-type stage. An inbound customer reply also reopens the ticket automatically in the first open stage of its current pipeline.

You can combine stage changes with assignee updates in the **same** `PATCH` request.

### 2D. Manage assignees

**Option A — via PATCH (common in integrations):**

```bash theme={null}
# Add one assignee
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "add_assignee_user_id": "660e8400-e29b-41d4-a716-446655440001" }'

# Remove one assignee
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "remove_assignee_user_id": "660e8400-e29b-41d4-a716-446655440001" }'

# Replace entire assignee list
curl -X PATCH "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "assignee_user_ids": ["660e8400-e29b-41d4-a716-446655440001"] }'
```

**Option B — dedicated assignee endpoints:**

```bash theme={null}
# POST /tickets/{id}/assignees  — body: { "user_id": "...", "role": "assignee" }
# DELETE /tickets/{id}/assignees — body: { "user_id": "..." }
# GET /tickets/{id}/assignees    — list active records
```

Re-adding an existing assignee is safe (idempotent).

***

## Part 3 — Communication threads (the conversation layer)

### Mental model

* One ticket can have **many threads** (e.g. email + WhatsApp).
* Each thread belongs to **one channel** and optionally maps to an external provider thread id.
* Messages always belong to **exactly one thread**.

```mermaid theme={null}
sequenceDiagram
  participant Provider as Email / WhatsApp provider
  participant API as Masivo API
  participant Agent as Agent dashboard

  Provider->>API: POST thread (first time)
  Provider->>API: POST message (inbound)
  API-->>Agent: TICKET_MESSAGE_RECEIVED event
  Agent->>API: POST message (outbound reply)
  API-->>Agent: TICKET_AGENT_REPLIED event
```

### 3A. List threads for a ticket

Returns summaries only — **not** message bodies.

```bash theme={null}
curl "https://app.masivo.ai/api/storefront/v1/communication/threads?ticket_id=TICKET_UUID&from=0&to=49" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID"
```

| Query param   | Default    | Purpose                                                  |
| ------------- | ---------- | -------------------------------------------------------- |
| `ticket_id`   | —          | Filter threads on this ticket                            |
| `review_id`   | —          | Filter by review                                         |
| `customer_id` | —          | Filter by customer external id                           |
| `channel`     | —          | `email`, `whatsapp`, `chat`, `review`, `system`, `other` |
| `status`      | —          | `open` or `closed`                                       |
| `from` / `to` | `0` / `49` | Pagination (max 100 rows per request)                    |

Example thread shell:

```json theme={null}
{
  "id": "8b3b9f2a-1c2d-4e5f-9a0b-1c2d3e4f5a6b",
  "ticket_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "channel": "email",
  "channel_account_id": "support@example.com",
  "external_thread_id": "mailgun-thread-abc123",
  "status": "open",
  "latest_message_preview": "I still have not received a refund.",
  "latest_message_at": "2026-06-25T15:00:00.000Z",
  "latest_message_direction": "inbound",
  "unread_count": 2,
  "metadata": {}
}
```

### 3B. Create a thread on a ticket

Do this **once per external conversation** before posting messages.

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/communication/threads" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "ticket_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "channel": "email",
    "channel_account_id": "support@example.com",
    "external_thread_id": "mailgun-thread-abc123",
    "metadata": { "provider": "mailgun" }
  }'
```

You must provide at least one of: `ticket_id`, `review_id`, `customer_id`, or `external_thread_id`.

If you POST again with the same `(channel, channel_account_id, external_thread_id)`, Masivo returns the **existing** thread — this is intentional.

### 3C. List messages in a thread

```bash theme={null}
curl "https://app.masivo.ai/api/storefront/v1/communication/threads/THREAD_UUID/messages?from=0&to=99" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID"
```

Messages are ordered chronologically (`sent_at`, then `created_at`). Pagination default: rows `0–99`, max range 200.

### 3D. Post messages

#### Inbound customer email (first message in thread)

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/communication/threads/THREAD_UUID/messages" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "external_message_id": "mailgun-msg-001",
    "direction": "inbound",
    "sender_type": "email",
    "sender_email": "maria@example.com",
    "sender_display_name": "María González",
    "subject": "Re: Order #8821 damaged",
    "body_text": "I have been waiting three days with no answer.",
    "status": "received",
    "raw_payload": { "provider": "mailgun", "id": "mailgun-msg-001" }
  }'
```

#### Agent outbound reply (linked to parent)

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/communication/threads/THREAD_UUID/messages" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "parent_message_id": "PARENT_MESSAGE_UUID",
    "external_message_id": "mailgun-msg-002-out",
    "direction": "outbound",
    "message_type": "reply",
    "sender_type": "agent",
    "sender_user_id": "660e8400-e29b-41d4-a716-446655440001",
    "body_text": "We are sorry — we are reviewing your case now.",
    "status": "sent"
  }'
```

If `message_type` is omitted and `parent_message_id` is set, Masivo defaults to `reply`.

#### Internal note (agents only — customer never sees this)

```bash theme={null}
curl -X POST "https://app.masivo.ai/api/storefront/v1/communication/threads/THREAD_UUID/messages" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "direction": "internal",
    "message_type": "internal_note",
    "sender_type": "agent",
    "sender_user_id": "660e8400-e29b-41d4-a716-446655440001",
    "body_text": "Customer is upset on email — prioritize WhatsApp.",
    "external_message_id": "internal-note-001"
  }'
```

Internal notes do **not** trigger `TICKET_CUSTOMER_REPLIED` or `TICKET_AGENT_REPLIED`.

### Message field cheat sheet

| Field                                  | When to use                                                                               |
| -------------------------------------- | ----------------------------------------------------------------------------------------- |
| `direction`                            | **Required.** `inbound` = customer→you, `outbound` = you→customer, `internal` = team-only |
| `external_message_id`                  | **Required for sync.** Provider's unique message id                                       |
| `parent_message_id`                    | When replying to a specific message in the **same thread**                                |
| `body_text` / `body_html` / `subject`  | At least **one** required                                                                 |
| `sender_user_id`                       | Agent UUID for outbound/internal                                                          |
| `sender_email` / `sender_display_name` | Customer identity for inbound email                                                       |
| `read_at`                              | Set on inbound when your UI marks as read (stops unread increment)                        |
| `raw_payload`                          | Store provider metadata for debugging                                                     |

### What updates automatically when you post a message

Masivo updates the parent thread:

* `latest_message_id`, `latest_message_at`, `latest_message_preview`
* `latest_message_channel`, `latest_message_direction`
* `unread_count` (+1 for inbound messages without `read_at`)

The ticket's `communication_summary` updates the next time you `GET /tickets/{id}`.

***

## Part 4 — End-to-end recipes

### Recipe A — Angry email, then resolve on WhatsApp

Real pattern from multi-channel support:

<Steps>
  <Step title="Create the ticket">
    `POST /tickets` with title/description and optional `customer_id`.
  </Step>

  <Step title="Create email thread">
    `POST /communication/threads` with `channel: email` and provider ids.
  </Step>

  <Step title="Ingest customer emails">
    For each inbound email, `POST …/messages` with unique `external_message_id`.
  </Step>

  <Step title="Create WhatsApp thread on same ticket">
    Second `POST /communication/threads` with `channel: whatsapp`.
  </Step>

  <Step title="Continue conversation on WhatsApp">
    Post inbound/outbound messages on the WhatsApp thread.
  </Step>

  <Step title="Resolve the ticket">
    `PATCH /tickets/{id}` with `to_stage_code: resolved` and `resolution_summary`.
  </Step>
</Steps>

Verify:

```bash theme={null}
# Should show thread_count: 2 and latest channel whatsapp
curl "https://app.masivo.ai/api/storefront/v1/tickets/TICKET_UUID" \
  -H "Authorization: Bearer $SERVER_API_KEY" \
  -H "x-account-id: $ACCOUNT_ID"
```

### Recipe B — Sync-only integration (your app owns the UI)

1. Webhook from email provider → map to `POST …/threads` (if new) + `POST …/messages`.
2. Poll `GET …/threads?ticket_id=` for unread badges.
3. When agent sends from your UI → `POST …/messages` with `direction: outbound`.
4. Stage changes → `PATCH /tickets/{id}`.

Always store Masivo's `thread.id` and `message.id` alongside provider ids in your database.

### Recipe C — Review → ticket → human follow-up

1. Customer submits review (existing review API).
2. `POST /tickets` with `review_id` — ticket + review thread created automatically.
3. Agent reads review in Conversation tab or via messages API.
4. Create additional threads (email/WhatsApp) if you contact the customer on other channels.
5. Resolve with `resolution_summary` when done.

***

## Part 5 — Working in the dashboard

| Area                             | What you can do                                                        |
| -------------------------------- | ---------------------------------------------------------------------- |
| **Tickets board**                | Kanban by stage; cards show communication preview and unread count     |
| **Ticket detail — Summary**      | Stats, profile header, linked channels                                 |
| **Ticket detail — Conversation** | Full timeline across all threads; filter by channel/direction; search  |
| **Ticket detail — Activity**     | Stage change history with who changed it and when                      |
| **Ticket detail — Sidebar**      | ID, status, stage, owner, assignees, ecommerce refs, resolution fields |
| **Tabs**                         | Resolution, associations, ticket info, metadata JSON                   |

Dashboard actions call the same backend rules as the API (required fields on resolve/close, assignee validation, etc.).

***

## Part 6 — Pagination and limits

| Endpoint                                   | Default range     | Max range |
| ------------------------------------------ | ----------------- | --------- |
| `GET /communication/threads`               | `from=0`, `to=49` | 100 rows  |
| `GET /communication/threads/{id}/messages` | `from=0`, `to=99` | 200 rows  |

Use `from`/`to` as inclusive row indices (not page numbers). Example: next page of threads → `from=50&to=99`.

***

## Part 7 — Errors and how to fix them

| HTTP  | Typical cause             | Fix                                                                                |
| ----- | ------------------------- | ---------------------------------------------------------------------------------- |
| `400` | Business rule failed      | Read `details` in body — e.g. missing `resolution_summary`, invalid parent message |
| `404` | Wrong id or wrong account | Check `x-account-id` and UUID                                                      |
| `422` | Validation failed         | Fix field types/enums; ensure `from <= to` and range limits                        |

Common `400` messages:

| Message                                                      | Meaning                                                      |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| `Parent message not found in this thread`                    | `parent_message_id` is wrong or belongs to another thread    |
| `Thread requires a ticket, review, customer, or external id` | Create-thread body missing context fields                    |
| `subject, body_text, or body_html is required`               | Empty message body                                           |
| Stage required field errors                                  | Pass `required_fields` when moving to `resolved` or `closed` |

***

## Part 8 — Automation hooks

Communication on ticket threads emits journey events you can use in [Ticket Journey Triggers](/guides/ticket-journey-triggers):

| Event                     | Use it to…                                          |
| ------------------------- | --------------------------------------------------- |
| `TICKET_CUSTOMER_REPLIED` | Escalate priority, notify assignee, start SLA timer |
| `TICKET_AGENT_REPLIED`    | Move to `waiting_customer`, send survey             |
| `TICKET_MESSAGE_RECEIVED` | Any inbound on ticket — broader than customer-only  |
| `TICKET_THREAD_ATTACHED`  | Welcome workflow when first channel linked          |

Ticket lifecycle events (`TICKET_STAGE_CHANGED`, `TICKET_RESOLVED`, etc.) work the same way.

***

## Frequently asked questions

<AccordionGroup>
  <Accordion title="What is the difference between a ticket and a thread?">
    The **ticket** is the case you track on the board (stage, priority, assignees, resolution). A **thread** is one conversation pipe on that case — usually one email chain or one WhatsApp chat. One ticket, many threads.
  </Accordion>

  <Accordion title="Why does GET /threads not return message text?">
    Performance. Thread list endpoints return **shells** with `latest_message_preview` only. Call `GET /communication/threads/{id}/messages` for full history.
  </Accordion>

  <Accordion title="Do I need external_thread_id and external_message_id?">
    If you integrate with an external inbox: **yes**. They prevent duplicates when webhooks retry. If you only use the Masivo dashboard manually, Masivo still generates internal UUIDs — but integrations should always send provider ids.
  </Accordion>

  <Accordion title="Can one message belong to two threads?">
    No. Each message belongs to exactly one thread. To link conversations across channels, attach multiple threads to the **same ticket**.
  </Accordion>

  <Accordion title="How does unread_count work?">
    Each **inbound** message without `read_at` increments the thread's `unread_count`. The ticket `communication_summary.unread_count` sums all threads on that ticket. Mark as read by posting/updating with `read_at` set (future API) or via dashboard consumption.
  </Accordion>

  <Accordion title="What happens if I create two tickets for the same review?">
    Masivo deduplicates review tickets — if an open ticket already exists for that `review_id`, the API returns the existing ticket instead of creating a second one.
  </Accordion>

  <Accordion title="Can I delete a thread or message via API?">
    Public Storefront endpoints documented here are create/list/get only. Soft-delete exists at the database layer for admin operations but is not exposed on these routes.
  </Accordion>

  <Accordion title="Which channel value should I use?">
    Use the channel that matches how the customer contacted you: `email`, `whatsapp`, `chat`, `review` (auto for review submissions), `system`, or `other`. The channel on a message defaults to its thread's channel if omitted.
  </Accordion>
</AccordionGroup>

***

## Checklist before going to production

* [ ] SERVER API key stored securely on your backend
* [ ] Every create-ticket call sends `idempotency_key`
* [ ] Every synced message sends `external_message_id`
* [ ] Every synced thread sends `external_thread_id` + `channel_account_id`
* [ ] Resolve/close flows collect `resolution_summary` / `closure_reason`
* [ ] Your UI loads thread shells first, messages on demand
* [ ] Journey automations tested for `TICKET_CUSTOMER_REPLIED` and stage changes

***

## Related documentation

* [Tickets & communication (concepts)](/concepts/tickets) — diagrams and field definitions
* [Ticket Journey Triggers](/guides/ticket-journey-triggers) — automate on ticket events
* [Create and manage review forms](/guides/create-and-manage-review-forms) — review → ticket pipeline
* API reference — **Tickets** group in the sidebar
