Contacts API
Search, create, update and enrich contacts; manage links, Do Not Contact, AI pauses, sharing and suggestion cards through the Goliath developer API
About
Look up and search contacts, create and update them, add notes and tags, assign teammates, find duplicate groups, manage saved filters, and skip-trace (enrich) contacts for phone numbers and emails. You can also manage links and related people, Do Not Contact and AI pauses, direct mail, sharing with partner organizations, and the suggestion cards AI employees leave on contacts.
All operations go through the developer API gateway — one endpoint, called by
operationId. If you haven’t set up a key yet, start with the
Developer API Overview.
Operations
Reading contacts (READ scope)
| operationId | What it does |
|---|---|
getContact |
Fetch one contact by id — identity, channels, addresses, properties, tags/lists, custom-field values, teammates, notes, and linked deals. Custom fields include the org’s definitions even when this contact’s values are null. A Do Not Contact record carries doNotContactSetAt and doNotContactSetBy (who set it). |
getContactIdentity |
A contact’s own email addresses — never its related people’s, and never one marked WRONG. Use it to match an inbound email to the right contact. |
getContactConnections |
A contact’s Links card (externalLinks), related people (linkedRelationships, with linkedContactId), relatives who are not contacts yet (subContacts), and any AI pause in force (aiPause). These are the ids the link and relationship writes take. A merged-away id resolves to the survivor. |
getContactsProperties |
Many contacts’ linked properties in one read: contactIds in, each contact’s properties[] out with propertyString and property.id (the id a skip trace, export or bulk selection takes; null when the address never matched a parcel). Contacts the key owner may not open are left out. |
findContacts |
Fuzzy-search contacts by name, phone, email, or address. Archived contacts are included by default, marked isArchived (pass includeArchived: false to drop them). Results are relevance-ranked candidates, not guaranteed exact matches; verify the returned name before using its contactId in a write. |
findContactsByNote |
Fuzzy-search note bodies from the last year and return one best snippet per matching contact. Searches at most 500 candidate notes per archive state and returns at most 50 contacts, so totalCount is not an exhaustive org-wide count. |
findDuplicateContactGroups |
Find groups with the same name (ignoring case/spacing) sharing a phone or email, including chained matches. Reads only active contacts visible to the key owner. Page groups with limit (default 50, max 100) and offset; truncated means totalCount is a floor. Name variants and property-only matches are not covered, so an empty result is not proof there are no duplicates. matchBy: NAME groups by name alone (unverified), listing the phones and emails members share; tagIds limits to groups with a member carrying one of those tags or on one of those lists. Review each group before mergeContacts. |
listContactActivities |
Page one contact’s calls, texts, emails, notes, and system events. Email entries include full plain-text textExtract when available; calls include disposition and disposition note. Filter by activity type and direction. |
countContacts |
The number of active contacts visible to your key, in one call — the same total the Contacts page shows. |
listContactFilters |
List saved contact filters visible to the key owner: shared organization filters plus their own private ones. Each row includes the owner’s userId; check it before editing a teammate’s filter. |
filterContacts |
Run a saved filterId or an unsaved filterTree, with limit/offset pagination and optional sort: { field, direction }. Returns contacts plus total and hasMore; an unsaved tree does not create a filter. Rows carry doNotContactReason and doNotContactSetAt. |
searchContactTags |
Find existing contact tags and lists by name. Use the returned ids in contact writes. |
contactTagUsage |
Free-form tags, least-used first, with contactCount (visible, unarchived), orgContactCount and dependents. maxContactCount: 0 lists unused tags; pass a filterTree to count only matching contacts per tag. Use it instead of counting contacts tag by tag. |
countRecords |
Counts grouped by any field in one call — tasks per contact, teammate or status; contacts by source; deals by stage. spec is a page data spec without fields (aggregates only), fenced to what the key owner may see. |
contactListUsage |
Every list, import upload list and export list with its member counts, emptiest first: visible unarchived contacts, plus organization-wide unarchived and archived counts and isEmpty (null unless the key owner can view all contacts). Narrow with kinds; onlyEmpty needs view-all access. |
listSmsOptOuts |
Who opted out of texts (texted STOP, asked to stop, carrier opt-out, a teammate’s block), newest first, with since and limit (default 25, max 100). Each row has source, optedOutAt, reason, phoneNumber and the visible contacts carrying it. totalCount and notVisibleCount are null unless the key owner can see every contact. Undeliverable numbers (landlines, dead numbers) are not opt-outs and are left out. |
duplicateContactsByRep |
findDuplicateContactGroups tallied per assigned rep (point person): how many duplicate groups hold each rep’s contacts. Unassigned contacts appear under a null rep. Optional matchBy as on findDuplicateContactGroups. |
findContactDataProblems |
Contact data-shape problems in one call — emails with capitals, empty or one-word names, phones not in the stored 10-digit form. Per problem: the count of visible contacts and a sample with the bad value. Pass problem to list up to 500 of its contact ids past offset (the input lowercaseContactEmails takes). |
findSharedPropertyAddresses |
Groups of two or more visible contacts linked to the same property address (by parcel, else mailing-address row, else normalized text), largest first, each member with how it is linked. Paged over groups with limit (default 50, max 100) and offset. |
searchInteractions |
Find contacts by what was said on calls, notes, texts or emails — pass the clues as a sentence in query, optionally narrowed by kinds, since or contactId. Verify each clue in the top results’ history before naming anyone. |
Use getFilter to read a saved criteria tree before editing it, and
saveFilter(type: CONTACT) to create one. Filter authoring and vocabulary
discovery are documented in the
Properties API.
filterContacts and filtered countContacts calls check the resolved
criteria against your organization’s plan. A field the plan excludes causes
an operation error with code PLAN_ENTITLEMENT_REQUIRED, rather than an
empty result. This applies to saved filters as well as inline trees; a saved
filter does not preserve access to a feature your plan no longer includes.
For filterContacts.sort, direction is ASC or DESC and field is
SELLER_INTENT_SCORE, LAST_COMMUNICATION, LAST_ACTIVITY, DIAL_ATTEMPTS
(organization-wide outbound dials), MY_LAST_CALL, MY_LAST_TEXT,
LAST_UPDATED, CREATED_AT, TASK_HIGHLIGHT, or AUCTION_DATE (earliest
upcoming auction or sale day, with contacts having none last). Unknown fields
are silently ignored and use the default order; a successful response does not prove a
guessed sort field worked.
Writing contacts (WRITE scope)
| operationId | What it does |
|---|---|
createContact |
Create a contact using input.sellerFirstName and input.sellerLastName. Optional addPhoneNumbers, addEmails, and address/custom-field inputs add details. type is optional and retained for compatibility; it does not set a deal type on the contact. |
updateContact |
Update one contact by contactId — identity, archive state, channels, addresses, properties, relatives, custom-field values, files, tags/lists, teammates, and teams. Add/remove fields are deltas, not replacements; the response echoes custom-field values for read-back. |
addContactNote |
Add a text note to a contact’s timeline. The response is the owning contact, not the note; report its echoed name and re-read the contact if you need the new note id. |
editContactNote |
Rewrite an existing note’s body by noteId. The note keeps its id and timeline position. Pass the full replacement body — this replaces the text, it doesn’t append. noteIds come from getContact (notes[]) or a NOTE entry in listContactActivities; addContactNote doesn’t return one, so re-read the contact to find the note you just wrote. |
deleteContactNote |
Permanently delete a note by noteId. No undo — when only the wording is wrong, use editContactNote instead. One note per call. Returns the owning contact with its remaining notes. |
changeNotePinned |
Pin (isPinned: true) or unpin a note by noteId. A pin notifies the contact’s teammates. |
createNoteReply |
Reply to a note as the key owner. Notifies the thread. Returns the replies; find yours by author and createdAt. |
editNoteReply |
Replace a reply’s body by replyId (from getContact notes[].replies). Same authorship rule as editContactNote. |
deleteNoteReply |
Delete a reply for good. Same authorship rule as editContactNote. Returns the replies left. |
mergeContacts |
Merge duplicates into one surviving record. contactIds is the full set (2 or more); primaryContactId is the survivor and must be one of them — it keeps its own custom fields and agent while absorbing the others’ notes, tags, and activities, and the losers are archived. name is the survivor’s resulting display name; optional pointPersonUserId decides which agent survives when the merged contacts disagree. Applied synchronously. |
addContactTags |
Apply existing tags (by tag id, from searchContactTags/createContactTag) to contacts, and new ones by exact name in newTagNames (created as it runs). Async — returns a bulkTaskId to poll with getBulkTask. |
removeContactTags |
Remove free-form tags from contacts without deleting the tag definition. Async; tag-removal workflows may run. Use ids from searchContactTags.tags — list/custom-field ids do nothing. |
addContactsToList |
Add contacts to one or more lists. Async; returns a bulkTaskId. List membership changes can trigger contact workflows. |
removeContactsFromList |
Remove contacts from lists without deleting the lists. Async and safe when a contact is already absent. |
assignContact |
Assign a teammate to contacts (default role POINT_PERSON; PARTICIPANT also allowed). Get userId from listTeammates. Async — returns a bulkTaskId. |
unassignContact |
Remove one or more teammates from contacts. Async; it does not hand work to a replacement, except that one remaining participant is promoted when they are the only assignee left. Re-read ownership afterward. |
archiveContacts |
Archive (soft-delete) contacts by id. Runs as an async bulk job — returns a bulkTaskId to poll with getBulkTask. Recoverable. |
unarchiveContacts |
Restore archived contacts by explicit id. Async. Restoring a duplicate archived by mergeContacts does not restore data moved to the survivor. |
renameContactTag |
Rename a free-form tag in place while preserving memberships, filters, and workflow references. |
renameContactList |
Rename a list in place while preserving memberships, filters, and workflow references. |
createContactTag |
Create an organization-wide free-form tag. Returns the tag catalog; read the new id back by name. |
createContactList |
Create an organization-wide contact list. Returns the list catalog; read the new id back by name. |
setContactsCustomField |
Set or clear one DROPDOWN custom field across many contacts. Values are option labels, not ids. Async; poll getBulkTask because validation failures surface on the worker. |
deleteContactTags |
Delete free-form tags by id (from createContactTag); also removes them from any contacts. |
deleteContactLists |
Delete contact lists by id (from createContactList); also drops membership. |
setContactsTeams |
Put contacts on teams (addTeamIds, from listTeams) and take them off (removeTeamIds); replaceExisting: true makes addTeamIds the exact set. addTeamIds is required — pass [] to only remove. Async; returns a bulkTaskId. |
lowercaseContactEmails |
Lowercase every capitalized email on up to 500 contacts in place, keeping each email’s id, ranking and verification. A contact already holding the lowercase twin keeps both. Get the ids from findContactDataProblems. |
removeContactFiles |
Take files off a contact’s Files & Photos by file id (a Library filing’s uploadedFileId, or a files[].id from getContact), as the Files card’s delete does. Returns the files the contact still has. |
controlContactsWorkflowRuns |
PAUSE or RESUME the workflow runs of explicit contacts; optional workflowGroupIds narrows it. RESUME only restarts runs someone paused by hand on a workflow version that is still active; other runs are skipped. Async; returns a bulkTaskId. |
deleteContactFilter |
Permanently delete a saved CONTACT filter by filterId; no archive or undo. Property/deal filter ids are refused. Check the owner’s userId in listContactFilters first: an org admin can delete a teammate’s shared filter, while a teammate’s private filter is refused. Prefer updateFilter when the criteria only need correcting. |
Custom fields (READ + WRITE scopes)
Custom fields are organization-wide definitions. These operations manage the
definitions themselves; you write per-contact values through the
addCustomFieldValues input on createContact and updateCustomFieldValues on
updateContact, using the customFieldId from here.
| operationId | Scope | What it does |
|---|---|---|
listContactCustomFields |
READ |
List the org’s contact custom-field definitions — id, name, type (TEXT | NUMBER | DOLLAR | DATE | LINK | DROPDOWN), dropdown options, allowMultiple, defaultValue, matchInboundTextsByValue and hideOnContactRecord. |
getContactFieldVisibility |
READ |
Each contact field’s show-when rule; null when the field is always shown. |
createContactCustomField |
WRITE |
Create a definition: name + type; options required for DROPDOWN, allowMultiple/defaultValue optional. Applied synchronously. Returns the org catalog — read the new id back by matching the name. |
updateContactCustomField |
WRITE |
Edit a definition in place by id — the right way to fix a wrong name, description, options, or default rather than creating a second field. Everything except the id is optional and omitted fields are untouched. type retypes the field in place (same id), refused once a contact has a value, anything uses it, it has a default, or it is a dropdown. options applies to DROPDOWN only and replaces the whole list, so send every option you want to keep, each existing one as { id, label }. hideOnContactRecord takes a field (a system one such as Source too) off the contact record without touching its values. matchInboundTextsByValue (TEXT fields only) links a text from an unknown number that quotes the field’s value to the contact holding it; it matches only 6–64 character values with no spaces that mix letters and digits. |
reorderContactCustomFields |
WRITE |
Set the display order. Presentation only — no definition, option, or recorded value is touched. Pass every custom-field id the org has, in the order you want: call listContactCustomFields immediately before and send the complete reordered list. A partial list, an unknown id, or a repeated id is rejected. |
addContactCustomFieldOption |
WRITE |
Add one option (label) to a contact DROPDOWN field without resending the whole list. |
setContactCustomFieldVisibility |
WRITE |
Show the fields in customFieldIds only when a dropdown holds any of optionIds. Display only; values are untouched. |
deleteContactCustomField |
WRITE |
Permanently delete a definition. Destructive and irreversible — every value recorded on every contact for this field is deleted with it and is not moved anywhere. Prefer updateContactCustomField when the field is merely wrong. Requires a dependencyResolution.impactVersion from getDeletionImpact (targetKind: CONTACT_CUSTOM_FIELD). |
Links and related people (WRITE scope)
Read the current links and relationships with getContactConnections.
| operationId | What it does |
|---|---|
addContactExternalLink |
Add a titled url to a contact’s Links card. |
updateContactExternalLink |
Change a contact link’s title or url; an omitted one is kept. |
removeContactExternalLink |
Remove a contact link. Irreversible. |
linkContactAsRelatedPerson |
Connect linkedContactId to parentContactId as a related person, with an optional free-text relation. A pair is unique in either direction: a repeat is refused as already linked. An archived or merged linkedContactId is refused. |
updateContactRelationship |
Set a related person’s relation label and note. Both are written, so an omitted one is cleared — send the current note to keep it. |
unlinkRelatedContact |
Disconnect a related person by relationshipId; both contacts stay. Irreversible. |
convertSubContactToLinkedRelationship |
Promote a relative listed on a contact (subContactId) to a contact of its own, linked back. linkToExisting: true folds it into the organization’s existing contact for the same person. |
Do Not Contact and AI pauses (WRITE scope)
| operationId | What it does |
|---|---|
setContactDoNotContact |
Mark a contact Do Not Contact (optional reason): every text, email and call to it is blocked, its workflow runs stop for good, and its AI employees come off. Use the id getContact returns. |
setPhoneDoNotContact |
Opt one phone number out of texts and calls for the whole organization, on every contact that carries it. |
pauseContactAi |
Pause AI on one contact. mode: ALL — no AI employee runs, and workflow AI-employee steps that come due are skipped, not deferred. mode: MESSAGES — runs go on, but no AI text, email or Slack message is suggested or sent. A future pausedUntil ends it on its own; null lasts until resumed. This is not Do Not Contact: people can still reach the contact. |
pauseContactsAi |
pauseContactAi for many contacts (same mode and pausedUntil). Async; returns a bulkTaskId. |
resumeContactAi |
Turn AI back on for one contact so its AI employees can text and email it again. A contact with no pause is a no-op. |
resumeContactsAi |
resumeContactAi for many contacts. Async; returns a bulkTaskId. |
Direct mail (READ + WRITE scopes)
| operationId | Scope | What it does |
|---|---|---|
listContactDirectMail |
READ |
The letters sent to one contact by any mail provider, newest first, 20 a page (pass nextCursor as after): id, campaign label, status, prepared and mailed dates. |
requestDirectMailCancellation |
WRITE |
Ask the mail provider to cancel one letter (pieceId from listContactDirectMail). It shows as canceled only once the provider confirms. Irreversible. |
Sharing with partner organizations (WRITE scope)
Share contacts with an organization you have an active partnership with — see
the Partnerships API and canShareWithPartners.
| operationId | What it does |
|---|---|
shareContactWithPartner |
Share one contact with a partner organization (partnerOrganizationId), as the contact page’s Share does: the partner gets its own copy, assigned to the partner users in assignments. The include* flags choose what goes with it (core record, tasks, notes, calls, texts). |
shareContactsWithPartner |
shareContactWithPartner for many contactIds. Async; returns a bulkTaskId. |
unshareContactWithPartner |
Stop sharing a contact: the partner’s users lose access to their copy. Sharing it again creates a new copy, never the old one back. |
Suggestion cards and Catch up (READ + WRITE scopes)
AI employees and Goliath leave suggestion cards on contacts — a drafted text or
email reply, a note, a task, a booking, a fix to the record. These operations
read and decide them as the app’s Catch up and contact card do. Catch up’s own
cards are decided through decideCatchUpCard (see the
Communications API).
| operationId | Scope | What it does |
|---|---|---|
listContactSuggestions |
READ |
A contact’s pending cards, newest first: what each proposes (action) and how sure it is. dirtyAt set means the conversation moved on since the card was written. |
acceptContactSuggestion |
WRITE |
Accept a card as written. A reply card sends its text or email to the contact; any other card applies its action. Optional senderUserId, replyInThread, acceptPairedActions, acknowledgedWarning. |
acceptEditedContactSuggestion |
WRITE |
Accept a text or email reply card with your own words (text) in place of the draft; it sends. |
decideSuggestion |
WRITE |
Accept or dismiss any card in the suggestion feed — a contact’s, an AI employee’s, or an account card — by id (verb: ACCEPT or DISMISS). A dismiss takes reason and optional note; an account card’s accept runs only selectedProposalKeys and needs an org admin; a card with a question of its own takes its answer in decisions. Returns id, kind and status. |
decideContactSuggestionsInBulk |
WRITE |
Accept or dismiss up to 500 cards by id, each as its own accept or dismiss would — an accept can send a reply per card. rowDecisions answers a card’s own question; dismissReason applies to a dismiss. Returns planned, applied, refused, truncated (only the first planned ran — call again) and unresolvedIds. An accept whose answer would spend credits is refused here. |
dismissContactSuggestion |
WRITE |
Dismiss a card with a reason (NOT_RELEVANT, WRONG_TONE, WRONG_FACTS, ALREADY_DONE, OTHER, …). |
markContactSuggestionHandled |
WRITE |
Close a card you already handled yourself (you sent the text, made the task): it is marked done without running its action, and a reply’s paired cards stay pending. |
setAsideContactSuggestions |
WRITE |
Take cards out of an import’s bulk review; they stay pending on each contact. Nothing is decided or spent. Returns setAside and unresolvedIds. |
commentOnSuggestionCard |
WRITE |
Leave feedback on a card (“too pushy”). Nothing changes or sends; the AI employee reads it when it drafts its next card. Send a new submissionKey (1–100 characters) per comment, and resend it unchanged on a retry. |
Enrichment (ADMIN scope)
| operationId | What it does |
|---|---|
enrichContact |
Skip-trace a contact to discover phone numbers and emails. Consumes one skip-trace credit (real money). |
createContactFromProperty |
Create a CRM contact from a property. Triggers skip-trace enrichment — consumes one skip-trace credit. |
Example
Find a contact, then add a note to it:
curl -X POST https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer gsk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "operationId": "findContacts", "variables": { "searchTerm": "maria alvarez" } }'
curl -X POST https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer gsk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "operationId": "addContactNote", "variables": { "contactId": "CONTACT_ID", "body": "Voicemail left 7/9 — call back Friday." } }'
Full variable schemas, response shapes, and worked examples for every operation are available from the discovery endpoint.