getEmailThread
Every message in one email thread, with full plain-text bodies, OLDEST FIRST.
Every message in one email thread, with full plain-text bodies, OLDEST FIRST (ordered by internalDate ascending): so the latest reply is the LAST element, not the first. Pass the emailThreadId from an EMAIL row in listInboxThreads. Each message carries subject, snippet, textExtract, fromEmail/fromName, toEmails, ccEmails, internalDate, direction, isRead, attachment file NAMES, and the campaign name when a workflow sent it. Read textExtract, not snippet, whenever the question is whether something appears in the thread: snippet stops after a line or two. textExtract is NULL on the HTML-only share of emails: for those the body is not readable through this API at all: never conclude their content is absent; say which you could not read. UNPAGINATED: one response holds every message. SCOPE: email is USER-owned, which is NARROWER than the org: a thread in a teammate's UNSHARED mailbox returns an EMPTY LIST even for a team admin, and even for a key owner holding VIEW_TEAM_CALLS_TEXTS or VIEW_ALL_PHONES (which widen phones and calls, not mailboxes). One exception: when the thread is one of several mailboxes' threads of ONE conversation, the response is that whole conversation: your own copies from any of its threads, plus copies held by mailboxes shared with your organization, with a each email returned once: a second stored copy of an email already in the response (a teammate's, or one in another of your mailboxes) is left out. So the same conversation comes back whole from EACH of its threads: dedupe by message id when reading several EMAIL rows. An empty list means "nothing here you may read" and collapses three cases on purpose: no such thread, not your mailbox, nothing left in it. It is never an error. Attachment BYTES are not downloadable through this API; only the file names are returned.
curl https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer $GOLIATH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operationId": "getEmailThread",
"variables": {
"threadId": "<threadId>"
}
}'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.getEmailThread({
threadId: '<threadId>'
})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: 'getEmailThread',
variables: {
threadId: '<threadId>'
}
}),
})
const { data, errors } = await res.json()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.
Response
What comes back under data: a list shows as one entry, and a value as its type.
{
"data": {
"inboxQuery": {
"getEmailThreadMessages": [
{
"id": "<ID>",
"threadId": "<ID>",
"subject": "…",
"snippet": "…",
"textExtract": "…",
"fromEmail": "…",
"fromName": "…",
"toEmails": [
"…"
],
"ccEmails": [
"…"
],
"internalDate": "2026-01-01T00:00:00.000Z",
"direction": "…",
"isRead": true,
"attachments": [
{
"id": "<ID>",
"fileName": "…"
}
],
"workflowAutomationName": "…"
}
]
}
}
}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.