getContact
MERGED IDS RESOLVE.
MERGED IDS RESOLVE: an id merged into another contact returns the SURVIVING record, so the id that comes back can differ from the one you sent. Use the returned id from then on, never the old one; it is not an error. A merge chain that leads nowhere answers as not-found. Fetch one contact by id, the full record: identity, emails, phones (with DNC status), mailing addresses, subject properties (property.id feeds getRelatedProperties), tags, lists, CUSTOM-FIELD VALUES, teammates + roles, teams, notes (with isPinned, isHidden and replies), linked deals (isArchived: true is not live), seller-intent score, enrichment/archive flags. A note is the team's record, never a message from the contact; calls are not here (listContactActivities [CALL]). A note's or reply's author { kind: USER | AI, id, name } is who WROTE it; read that, not createdByUser, which is null on a note an AI assistant wrote for a teammate. A hidden note or reply is shown only to admins; replies also omits ones you may not see (e.g. an outside author's), so a missing reply is not proof it is gone. CUSTOM FIELDS: the ONLY read-back for values written via createContact.addCustomFieldValues / updateContact.updateCustomFieldValues (listContactCustomFields returns definitions, never values). customFields is the organization's field catalog, not this contact's answers, listed whether or not the contact has one, so its length says nothing about the contact (fields sharing a normalized name collapse to one entry, so it can differ from listContactCustomFields). The value is in values, NULL when this contact has none: null means UNSET, not missing, unreadable or an error. Read the ONE sub-field matching the field type; the other three stay null on every type: TEXT and LINK → values.textValue; NUMBER and DOLLAR → values.numberValue (DOLLAR is a plain number as written, NOT cents); DATE → values.dateValue (ISO 8601); DROPDOWN → values.dropdownSelectedValues, the selected option LABELS (not option ids), at most one entry unless allowMultiple is true. Reading the wrong one reports an unset field for a contact that has an answer. A LINK value is stored normalized (a bare host gains its scheme), so compare what you wrote with what comes back rather than assume byte equality.
curl https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer $GOLIATH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operationId": "getContact",
"variables": {
"contactId": "<contactId>"
}
}'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.contacts.getContact({
contactId: '<contactId>'
})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: 'getContact',
variables: {
contactId: '<contactId>'
}
}),
})
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": {
"contactQuery": {
"contact": {
"id": "<ID>",
"name": "…",
"createdAt": "2026-01-01T00:00:00.000Z",
"isArchived": true,
"isEnriched": true,
"sellerIntentScore": 0,
"doNotContact": true,
"doNotContactReason": "…",
"doNotContactSetAt": "2026-01-01T00:00:00.000Z",
"doNotContactSetBy": {
"id": "<ID>",
"firstName": "…",
"lastName": "…"
},
"emails": [
{
"id": "<ID>",
"email": "…",
"verificationStatus": "VERIFIED"
}
],
"phoneNumbers": [
{
"id": "<ID>",
"phoneNumber": "…",
"phoneType": "MOBILE",
"verificationStatus": "VERIFIED",
"dncStatus": {
"isDnc": true,
"isLitigator": true
}
}
],
"mailingAddresses": [
{
"id": "<ID>",
"rawText": "…",
"verificationStatus": "VERIFIED",
"address": {
"addressFull": "…"
}
}
],
"properties": [
{
"id": "<ID>",
"propertyString": "…",
"status": "…",
"verificationStatus": "VERIFIED",
"property": {
"id": "<ID>"
}
}
],
"tags": [
{
"id": "<ID>",
"name": "…"
}
],
"lists": [
{
"id": "<ID>",
"name": "…"
}
],
"customFields": [
{
"id": "<ID>",
"name": "…",
"type": "DROPDOWN",
"allowMultiple": true,
"values": {
"textValue": "…",
"dateValue": "2026-01-01T00:00:00.000Z",
"numberValue": 0,
"dropdownSelectedValues": [
"…"
]
}
}
],
"participants": [
{
"status": "REVOKED",
"user": {
"id": "<ID>",
"firstName": "…",
"lastName": "…",
"email": "…"
}
}
],
"userGroups": [
{
"id": "<ID>",
"name": "…"
}
],
"notes": [
{
"id": "<ID>",
"body": "…",
"createdAt": "2026-01-01T00:00:00.000Z",
"author": {
"kind": "USER",
"id": "<ID>",
"name": "…"
},
"createdByUser": {
"id": "<ID>",
"firstName": "…",
"lastName": "…"
},
"isPinned": true,
"isHidden": true,
"replies": [
{
"id": "<ID>",
"body": "…",
"isHidden": true,
"author": {
"kind": "USER",
"id": "<ID>",
"name": "…"
}
}
]
}
],
"deals": [
{
"id": "<ID>",
"title": "…",
"priceCents": "…",
"isArchived": true,
"priceFormatted": "…",
"stage": {
"id": "<ID>",
"name": "…"
}
}
]
}
}
}
}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.