Skip to main content
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) first, then come back here for practical steps.

Before you start

1

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.
2

Know the base URL

All examples use:
https://app.masivo.ai/api/storefront/v1
Every request also requires:
Authorization: Bearer <SERVER_API_KEY>
x-account-id: <ACCOUNT_UUID>
Content-Type: application/json
3

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.

Quick reference — all ticket & communication endpoints

MethodPathWhat it does
GET/ticket-pipelinesList pipelines and ordered stages, including internal open/closed state
POST/ticketsCreate 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}/assigneesList active assignee records
POST/tickets/{id}/assigneesAdd one assignee or watcher
DELETE/tickets/{id}/assigneesRemove an assignee
GET/communication/threadsList thread shells (summaries, no message bodies)
POST/communication/threadsAttach a new thread to a ticket (or review/customer)
GET/communication/threads/{id}Get one thread shell
GET/communication/threads/{id}/messagesList messages in chronological order
POST/communication/threads/{id}/messagesAdd inbound, outbound, or internal message
API reference pages with schemas: Tickets group in the sidebar.

Part 1 — Creating tickets

1A. Find the pipeline and stage ids

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.
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"
  }'
FieldRequired?Notes
titleYesMax 200 characters
descriptionNoMax 5000 characters
pipeline_idNoPipeline UUID from GET /ticket-pipelines; defaults to the account pipeline
stage_codeNoInternal stage code in that pipeline; defaults to its default stage
priorityNolow, medium, high, urgent — default medium
source_typeNoDefault manual
review_idNoIf set, creates a review ticket instead (see below)
customer_idNoExternal customer id; stored as a customer association
assignee_user_idsNoArray of team member UUIDs
actor_user_idNoWho performed the action; defaults to a system actor
idempotency_keyNoStrongly recommended. Re-posting the same key returns the original ticket
required_fieldsNoRequired 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
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)

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):
{
  "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

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)

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:
{
  "to_pipeline_id": "880e8400-e29b-41d4-a716-446655440003",
  "to_stage_code": "a2ac1542-934c-4b8f-b8cf-4a64ba923fb5"
}
Resolving — you must include a resolution summary:
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:
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):
# 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:
# 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.

3A. List threads for a ticket

Returns summaries only — not message bodies.
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 paramDefaultPurpose
ticket_idFilter threads on this ticket
review_idFilter by review
customer_idFilter by customer external id
channelemail, whatsapp, chat, review, system, other
statusopen or closed
from / to0 / 49Pagination (max 100 rows per request)
Example thread shell:
{
  "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.
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

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)

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)

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)

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

FieldWhen to use
directionRequired. inbound = customer→you, outbound = you→customer, internal = team-only
external_message_idRequired for sync. Provider’s unique message id
parent_message_idWhen replying to a specific message in the same thread
body_text / body_html / subjectAt least one required
sender_user_idAgent UUID for outbound/internal
sender_email / sender_display_nameCustomer identity for inbound email
read_atSet on inbound when your UI marks as read (stops unread increment)
raw_payloadStore 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:
1

Create the ticket

POST /tickets with title/description and optional customer_id.
2

Create email thread

POST /communication/threads with channel: email and provider ids.
3

Ingest customer emails

For each inbound email, POST …/messages with unique external_message_id.
4

Create WhatsApp thread on same ticket

Second POST /communication/threads with channel: whatsapp.
5

Continue conversation on WhatsApp

Post inbound/outbound messages on the WhatsApp thread.
6

Resolve the ticket

PATCH /tickets/{id} with to_stage_code: resolved and resolution_summary.
Verify:
# 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

AreaWhat you can do
Tickets boardKanban by stage; cards show communication preview and unread count
Ticket detail — SummaryStats, profile header, linked channels
Ticket detail — ConversationFull timeline across all threads; filter by channel/direction; search
Ticket detail — ActivityStage change history with who changed it and when
Ticket detail — SidebarID, status, stage, owner, assignees, ecommerce refs, resolution fields
TabsResolution, 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

EndpointDefault rangeMax range
GET /communication/threadsfrom=0, to=49100 rows
GET /communication/threads/{id}/messagesfrom=0, to=99200 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

HTTPTypical causeFix
400Business rule failedRead details in body — e.g. missing resolution_summary, invalid parent message
404Wrong id or wrong accountCheck x-account-id and UUID
422Validation failedFix field types/enums; ensure from <= to and range limits
Common 400 messages:
MessageMeaning
Parent message not found in this threadparent_message_id is wrong or belongs to another thread
Thread requires a ticket, review, customer, or external idCreate-thread body missing context fields
subject, body_text, or body_html is requiredEmpty message body
Stage required field errorsPass 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:
EventUse it to…
TICKET_CUSTOMER_REPLIEDEscalate priority, notify assignee, start SLA timer
TICKET_AGENT_REPLIEDMove to waiting_customer, send survey
TICKET_MESSAGE_RECEIVEDAny inbound on ticket — broader than customer-only
TICKET_THREAD_ATTACHEDWelcome workflow when first channel linked
Ticket lifecycle events (TICKET_STAGE_CHANGED, TICKET_RESOLVED, etc.) work the same way.

Frequently asked questions

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.
Performance. Thread list endpoints return shells with latest_message_preview only. Call GET /communication/threads/{id}/messages for full history.
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.
No. Each message belongs to exactly one thread. To link conversations across channels, attach multiple threads to the same ticket.
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.
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.
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.
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.

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