Skip to main content
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
The envelope (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 Z for 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 T or by a single space (2025-07-02 14:30:00).
  • A bare date — 2025-07-02 — is accepted; the time becomes 00:00 UTC.
Prefer the explicit-offset form. An unparseable value becomes null for that field, and no error is raised — the contact is still ingested.
Consequence when this hits updated_at: a null updated_at carries no modification time, so the contact cannot advance the watermark and is not selected by any later updated_since filter. In effect the contact — and every future change to it — drops out of incremental sync. Keep updated_at a valid timestamp that advances on every change.

Fields the adapter ignores

Sending any of these has no effect — do not spend effort populating them:

Identity & cross-references

  • Identity: id is the stable primary key. HunterAI upserts by (connection, id), so a contact’s id must never change — a changed id is ingested as a brand-new contact.
  • responsible_user_id → resolves against /users.
  • Referenced by leads: a lead’s first contact_ids entry resolves to a contact id returned 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_at drives the per-entity watermark — the largest updated_at HunterAI has committed. The next run resumes from there.
  • You must return contacts ordered by updated_at ascending, so the watermark can advance page by page without skipping an older contact on a later page.
  • updated_at must change on every modification of a contact — any field edit. A change that does not advance updated_at falls at or below the watermark and is never pulled again.
See How sync works for the shared model.

Deletion behaviour

Contacts carry no deletion signal at all. Unlike leads, there is no is_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-length id — none of the contacts on that page are saved. This is page-level, not per-record.
  • A failed page fails the sync. Because /contacts is 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 null and the contact is still ingested. See the consequence for updated_at above.
  • A non-object custom_fields does not fail anything — it is stored as null and 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 a 5xx. 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.

If this endpoint is missing

/contacts is required. A 404 fails the contacts entity and the sync is reported failed. Leads still ingest, but the people behind them are unresolved, so contact-level context and per-contact attribution are empty.