GraphQL API

Managing the Inbox

Read state, close and reopen, assignment, blocking, the AI toggle, chat fields, labels and collections

Everything that changes an inbox row's state rather than its messages.

Scope: CONVERSATIONS_MANAGE for everything on this page.

Read State

readMessages(ids: [String!]!): Int!
readAllMessages(chatId: String!): Float!
OperationDescription
readMessagesMarks specific messages read. Takes message IDs, returns how many changed.
readAllMessagesMarks the whole thread read. Returns how many changed.
mutation MarkRead {
  readAllMessages(chatId: "6804f4c2a6f9f35f6e66f1a1")
}

Close and Reopen

closeChat(chatId: String!): Chat!
reopenChat(chatId: String!): Chat!

Both act on a thread and return the updated Chat. The conversation's own status is a rollup over its threads.

Assignment

assignChatToOperator(chatId: String!, operatorId: String!): Chat!
unassignChat(chatId: String!): Chat!
assignConversationThreads(conversationId: String!, operatorId: String): Int!
OperationDescription
assignChatToOperatorAssigns one thread to an operator.
unassignChatClears the assignment on one thread.
assignConversationThreadsAssigns every thread of a conversation at once. Returns how many changed. Pass operatorId: null to unassign them all.

operatorId is a user ID, not a member ID. Get it from orgTeamMembers - the userId field, which exists for exactly this purpose.

mutation Assign {
  assignConversationThreads(
    conversationId: "6804f4d5a6f9f35f6e66f1c4"
    operatorId: "66120a4e1c34d0012ab87001"
  )
}

Blocking

blockConversation(conversationId: String!, blocked: Boolean!): Int!

Blocks or unblocks the person behind the conversation, not a single thread. Returns how many threads were affected.

mutation Block {
  blockConversation(conversationId: "6804f4d5a6f9f35f6e66f1c4", blocked: true)
}

On the conversation, isBlocked reflects the person-level flag and hasBlockedThread is true when any thread is blocked.

AI Toggle

setConversationAi(conversationId: String!, enabled: Boolean!): Int!

Turns the org's AI assistant on or off for this conversation. Returns how many threads changed.

Use it to hand a conversation from a bot to a human, or back:

mutation HandOff {
  setConversationAi(conversationId: "6804f4d5a6f9f35f6e66f1c4", enabled: false)
}

Chat Fields

updateChatField(input: UpdateChatFieldInput!): Boolean!

Sets one field on a chat. Returns true when the write landed.

UpdateChatFieldInput

FieldTypeRequiredDescription
chatIdString!YesThe thread to update.
fieldChatFieldKey!YesWhich field - see below.
valueJSONNoThe new value. Its shape depends on the field.
blockScopeBlockScopeNoOnly meaningful with IS_BLOCKED: CONTACT blocks the person, THREAD blocks just this thread.

ChatFieldKey values:

KeySets
CHAT_STATUSThe thread's status.
PRIORITYPriority.
CHAT_CATEGORYCategory.
IS_BLOCKEDBlocked state - pair it with blockScope.
AI_AVAILABLEWhether the AI assistant may answer here.
VERIFIEDVerified mark.

One field per call.

mutation Prioritise {
  updateChatField(input: {
    chatId: "6804f4c2a6f9f35f6e66f1a1"
    field: PRIORITY
    value: "HIGH"
  })
}

IS_BLOCKED and AI_AVAILABLE overlap with blockConversation and setConversationAi. The difference is the level: updateChatField acts on one thread (and blockScope lets you choose person or thread), while the conversation mutations act across every thread of the conversation at once.

Retrying a Failed Send

retryFailedMessage(messageId: String!): Message!

MESSAGES_SEND

Retries one message that failed. Only a failed outbound message qualifies:

TargetResult
A failed outbound messageRetried.
An inbound message, or one that did not fail400
A malformed, missing or foreign idThe same not-found either way.

Check the failure first. A message whose errorMessage starts with outcome unknown may already have reached the contact - retrying it risks a duplicate. See delivery status.

Flows in a Chat

apiStartFlowForChat(chatId: String!, flowId: String!): Chat!
apiStopFlowForChat(chatId: String!): Chat!

CONVERSATIONS_MANAGE

RuleDetail
The flow must be publishedA draft, archived or foreign flow is one not-found.
Not in a group chatA key starting a flow in a group chat gets a 400.
Flows are not listable over the APITake the flowId from the dashboard.

Both return the chat with its activeFlow - flow id, title, execution id, initiator and start time. A key-started run is recorded with initiator WEBHOOK and platform api.

The same ownership-and-published check now applies to members too: starting another org's flow, or a draft, is a not-found in the dashboard as well.

Deleting

Both operations are irreversible and need the separate CONVERSATIONS_DELETE scope - CONVERSATIONS_MANAGE does not imply it. Both are expensive, and every attempt writes an audit line.

apiDeleteChat(chatId: String!): DeleteChatResult!
apiDeleteConversation(conversationId: String!, removePeople: Boolean!): PartnerDeleteConversationResult!

apiDeleteChat

CONVERSATIONS_DELETE

Deletes one thread with its messages and media. The contact stays, and so do their other threads. Returns { messages, media } - what was removed.

apiDeleteConversation

CONVERSATIONS_DELETE; with removePeople: true also CONTACTS_ERASE

Deletes the conversation with all its threads, messages and media. Returns { chats, messages, media, contactsErased }.

removePeople is the part to read carefully:

removePeopleWhat happens
falseThe conversation and its content go. The contacts remain intact.
trueA GDPR erase as well: the conversation's contacts are anonymised everywhere in the org, and an erasure log is written.

removePeople: true is not "also delete these contacts" - it is an erase with org-wide reach. The contacts' other conversations stay, but anonymised. Use it only for a genuine deletion request, and prefer the dedicated erase flow when erasure is what you actually mean.

Errors

SituationResponse
removePeople: true without CONTACTS_ERASE403, decided before any id is looked up
A contact of the conversation is mid-merge409 retry shortly - nothing is deleted or erased
A direct thread with no contact409 - nothing is deleted or erased

Labels and Collections

A collection is a saved view of the inbox - what the dashboard shows as a label or folder. It can be a manual list or rule-driven.

apiCreateChatCollection(input: CreateChatCollectionInput!): ChatCollection!
apiUpdateChatCollection(idOrSlug: String!, input: UpdateChatCollectionInput!): ChatCollection!
apiMoveChatCollection(input: MoveCollectionInput!): ChatCollection!
apiDeleteChatCollection(idOrSlug: String!): Boolean!
OperationDescription
apiCreateChatCollectionCreates a collection.
apiUpdateChatCollectionUpdates one, addressed by ID or slug.
apiMoveChatCollectionReorders it in the sidebar.
apiDeleteChatCollectionDeletes it. The conversations inside are untouched.

The read side - labels, activeLabels, label - needs MESSAGES_READ; creating, updating, archiving, restoring and setMessageLabels need CONVERSATIONS_MANAGE. A malformed label id is a not-found, not a server error, and a key without NOTES_WRITE cannot label an internal note or a system row.

CreateChatCollectionInput takes name (required), plus optional slug, description, icon, color, position, and rules - the same CollectionRuleInput shape as message search, which makes the collection rule-driven instead of manual.

ChatCollection carries _id, name, slug, description, icon, color and position.

Addressing by slug means you can write apiUpdateChatCollection(idOrSlug: "urgent", …) without storing IDs. The same slug works as collectionIdOrSlug in message search.

What These Emit

State changes fan out to your webhook as chat.updated and conversation.updated. Expect one inbox action to produce more than one event - deduplicate on the envelope id and reconcile by timestamp, never by arrival order.

On this page