listInboxThreads
The inbox.
The inbox: text conversations, calls and email threads in ONE most-recent-first feed, the read for questions that name no contact ("did they text back", "what is unread"; listContactActivities needs a contactId). Each row carries the channel in type (MESSAGE | CALL | EMAIL) and only the fields that channel has: a MESSAGE row carries mostRecentMessage (with its full body) + replySentiment; a CALL row carries status/durationSeconds/summary/hasRecording and, for a grouped day of calls, groupCount + groupedCallIds; an EMAIL row carries subject/snippet/fromEmail/toEmails/direction (of its latest message) + emailThreadId. Quote createdAtLocal; never convert a UTC time yourself. Rows are PREVIEWS; a MESSAGE row's recentInbound holds the contact's newest texts (up to 3), which a later send hides from mostRecentMessage. To read a whole conversation, follow the id: a MESSAGE row's id is the conversationId for getTextThreadMessages, an EMAIL row's emailThreadId is the threadId for getEmailThread, and a CALL row (or an id from groupedCallIds) goes to getCallRecording. WHO THE CONTACT IS: on MESSAGE and CALL rows potentialContacts ({ id, name }) lists the contacts on that number: one entry names the contact; several mean the number is shared, so name them all or open the thread rather than picking the first; empty means none matches. contactName is caller-ID only, set for an unmatched inbound CALL, and is null on every text row, so a null contactName never means the contact is unknown. No channel reads all three channels in ONE call. FILTERS: seen: false is unread (one flag across all three channels (a text conversation's seen, a call's seen, an email's isRead); channel narrows to CALLS | MESSAGES | EMAILS | MISSED_CALLS | UNRESPONDED | NEEDS_RESPONSE (TEXT threads only: the contact spoke last, less ones a teammate marked handled; for email and calls read EMAILS (INBOUND direction: they wrote last) and MISSED_CALLS); respondedOnly: true keeps only threads the contact has replied on at least once; sentBy keeps threads with an outbound message from HUMAN, AI_EMPLOYEE and/or WORKFLOW (a campaign)) sentBy: [WORKFLOW], respondedOnly: true is "who replied to a campaign"; replySentiments keeps threads whose most recent inbound reply was classified POSITIVE | NEUTRAL | NEGATIVE | OPT_OUT | UNREACHABLE, and POSITIVE also keeps any thread that replied POSITIVE and has not opted out since; optedOut is INCLUDE by default (every row), EXCLUDE leaves out the opted-out text threads the app inbox hides (the contact said stop, the number is SMS-suppressed or the number's contacts are all Do Not Contact, with nothing from them since), and ONLY returns just those. WHICH FILTERS REACH CALLS: setting respondedOnly or replySentiments drops every CALL row (a call has no reply). sentBy DOES filter calls (AI_EMPLOYEE when the call has an AI-employee author, else HUMAN), but no call matches WORKFLOW. searchTerm matches message text, an email's subject and latest snippet, and participant names (a term that names a contact reads only their threads); a short page with a cursor left means ask again. starredOnly: true keeps only starred rows of any channel (each has isStarred; a MESSAGE row also isNeedsReplyHandled when a teammate marked it handled). PAGING is keyset per channel: pass nextCallCursor/nextSmsCursor/nextEmailCursor back as callCursor/smsCursor/emailCursor, ALL THREE exactly as received (a null one as null: alone it is no finished channel); dropping a non-null cursor restarts that channel and repeats rows. THE FEED IS DONE only when ALL THREE come back null: never stop on an empty page while one remains. limit is per page across the merged feed (default 25, max 50). SCOPE: with no userIds this is the KEY OWNER'S OWN inbox (their lines and mailbox, as the app shows them). Naming a teammate in userIds reads their inbox only if the key owner holds VIEW_TEAM_CALLS_TEXTS or VIEW_ALL_PHONES (a teammate's email only from a mailbox they share); without either the request is REFUSED (an error), which is different from an empty page. An EMPTY list means nothing in your own inbox matched, never that the org has no messages. Teammate ids come from listTeammates.
curl https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer $GOLIATH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operationId": "listInboxThreads",
"variables": {}
}'import { GoliathClient } from '@goliath-data/sdk'
const goliath = new GoliathClient({
apiKey: process.env.GOLIATH_API_KEY!,
baseUrl: 'https://server.goliathdata.com',
})
const result = await goliath.conversations.listInboxThreads({})const res = await fetch('https://server.goliathdata.com/api/v1/call', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GOLIATH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
operationId: 'listInboxThreads',
variables: {}
}),
})
const { data, errors } = await res.json()limit is clamped to 50.
Variables
The example sends only what the schema requires. Some operations also need one of several optional variables (one filter or another, say); the description above says which.
Default 25.
Default DESC.
Default ALL.
Classification of an inbound reply's intent. Mirrors the Prisma `ReplySentiment` enum 1:1. POSITIVE/NEUTRAL/NEGATIVE are the genuine-reply quality buckets; OPT_OUT (a compliance event) and UNREACHABLE (wrong number / auto-reply / OOO) are kept separate so they never fold into "negative".
Default INCLUDE.
Response
What comes back under data: a list shows as one entry, and a value as its type.
{
"data": {
"inboxQuery": {
"paginateInbox": {
"items": [
{
"id": "<ID>",
"type": "CALL",
"createdAt": "2026-01-01T00:00:00.000Z",
"createdAtLocal": "…",
"seen": true,
"contactName": "…",
"contactE164PhoneNumber": "…",
"phoneLineId": "…",
"emailThreadId": "…",
"potentialContacts": [
{
"id": "<ID>",
"name": "…"
}
],
"direction": "INBOUND",
"mostRecentMessage": {
"id": "<ID>",
"message": "…",
"direction": "INBOUND",
"status": "PENDING",
"createdAt": "2026-01-01T00:00:00.000Z",
"createdAtLocal": "…",
"author": {
"kind": "USER",
"id": "<ID>",
"name": "…"
}
},
"recentInbound": [
{
"body": "…",
"at": "2026-01-01T00:00:00.000Z",
"atLocal": "…"
}
],
"replySentiment": "POSITIVE",
"isStarred": true,
"isNeedsReplyHandled": true,
"status": "RINGING",
"durationSeconds": 0,
"summary": "…",
"hasRecording": true,
"groupCount": 0,
"groupedCallIds": [
"<ID>"
],
"author": {
"kind": "USER",
"id": "<ID>",
"name": "…"
},
"user": {
"id": "<ID>",
"firstName": "…",
"lastName": "…"
},
"subject": "…",
"snippet": "…",
"fromEmail": "…",
"fromName": "…",
"toEmails": [
"…"
],
"isRead": true
}
],
"nextCallCursor": {
"date": "2026-01-01T00:00:00.000Z",
"id": "<ID>"
},
"nextSmsCursor": {
"date": "2026-01-01T00:00:00.000Z",
"id": "<ID>"
},
"nextEmailCursor": {
"date": "2026-01-01T00:00:00.000Z",
"id": "<ID>"
}
}
}
}
}Errors
unknown_variableA variable the operation does not declare. A typo is refused rather than silently dropped.
unauthorizedNo bearer header, or the key is invalid or revoked.
insufficient_scopeThe key lacks the scope the operation requires.
forbiddenA referenced record does not exist, was deleted, or is outside your organization. The gateway cannot tell those apart.
Behaviour
A read. It rejects an Idempotency-Key header with 400 bad_request.