Required.
/contacts returns your contacts — the people behind your leads.
It is incremental: it receives
updated_since and must be returned ordered by updated_at ascending.Purpose
A contact is one person in your CRM. HunterAI reads/contacts to resolve who is
behind each lead — a lead’s first contact_ids entry points here — and to carry
contact-level context (name, phone, email, and your custom fields) for analytics
and coaching. Contacts share the same incremental contract as
/leads: the same updated_since filter, the same required
ascending order, and the same watermark rules.
Request
You must return the contacts ordered by
updated_at ascending. HunterAI
advances its watermark page by page and relies on that order; an out-of-order
page can cause older contacts on a later page to be skipped.
Response envelope
200 OK
data, has_more, page, limit) is the same on every endpoint
— see How sync works → The envelope.
Only data and has_more are read.
Fields
custom_fields must be a JSON object. Each value inside it may be any JSON
type — string, number, boolean, object, array, or null — and is stored
unchanged. If custom_fields itself is not an object — an array, a string,
a number, a boolean, or an empty {} — it is silently stored as null (the
whole field is dropped); no error is raised and the contact is still
ingested.Timestamp format
created_at and updated_at accept a calendar date and time in these concrete
forms:
- With an explicit offset —
2025-07-02T14:30:00+05:00. - With a trailing
Zfor UTC —2025-07-02T14:30:00Z. - With no zone at all —
2025-07-02T14:30:00, which is read as UTC. - The date and time may be separated by a
Tor by a single space (2025-07-02 14:30:00). - A bare date —
2025-07-02— is accepted; the time becomes00:00UTC.
null for that
field, and no error is raised — the contact is still ingested.
Fields the adapter ignores
Sending any of these has no effect — do not spend effort populating them:is_deletedor any other deletion flag — contacts have no tombstone; see Deletion behaviour.- Any other key not listed in the Fields table — contacts retain no raw copy, so anything unlisted is dropped entirely. See Limitations → Fields dropped entirely.
Identity & cross-references
- Identity:
idis the stable primary key. HunterAI upserts by(connection, id), so a contact’sidmust never change — a changedidis ingested as a brand-new contact. responsible_user_id→ resolves against/users.- Referenced by leads: a lead’s first
contact_idsentry resolves to a contactidreturned here — HunterAI keeps only that first element. See/leads.
Incremental sync
/contacts is incremental, on the same contract as /leads.
HunterAI sends updated_since; return only contacts whose updated_at is
greater than or equal to it (inclusive — re-returning the boundary contact is
harmless, since the upsert is idempotent).
updated_atdrives the per-entity watermark — the largestupdated_atHunterAI has committed. The next run resumes from there.- You must return contacts ordered by
updated_atascending, so the watermark can advance page by page without skipping an older contact on a later page. updated_atmust change on every modification of a contact — any field edit. A change that does not advanceupdated_atfalls at or below the watermark and is never pulled again.
Deletion behaviour
Contacts carry no deletion signal at all. Unlike leads, there is nois_deleted on a contact and no other tombstone — an is_deleted: true you send
is ignored, not honoured. Because the upsert only inserts and updates, a contact
you delete in your CRM, or simply stop returning, is not removed from
HunterAI; it stays ingested as an active contact and its historical row persists.
Only leads support deletion, via is_deleted — see
Limitations → Tombstones.
Failure modes
A page of contacts (up to 100 records) is validated and written as one transaction. The consequences:- A single malformed contact fails the whole page. If any contact on a page
cannot be processed — a missing
id, or an over-lengthid— none of the contacts on that page are saved. This is page-level, not per-record. - A failed page fails the sync. Because
/contactsis required, a page that fails takes the contacts entity down with it, and the overall sync is reported failed. Pages that already committed stay committed, and the next run resumes from the watermark. - An unparseable timestamp does not fail anything — the field becomes
nulland the contact is still ingested. See the consequence forupdated_atabove. - A non-object
custom_fieldsdoes not fail anything — it is stored asnulland the contact is still ingested. 429,5xx, and request timeouts are retried with exponential backoff up to 5 times, then the entity fails; a timeout is retried the same way as a5xx. HunterAI applies its request rate limit per connection, so two connections to the same base URL are throttled independently.- A repeated or unbounded page — a page whose IDs match the previous page, or a run past the page ceiling — fails the entity. See How sync works → Paging.