WarmblyDocs
Endpoint reference

Contacts

Search, manage, import, export, and enrich contacts along with their notes, activities, timeline, and deals.

Contacts are the people you send to. This group covers the full lifecycle: searching and filtering, creating and editing (singly and in bulk), CSV/XLSX/JSON import and export, the hydrated contact 360 view, the per-contact email and activity feeds, and CRM notes, activities, and deals attached to a contact. List endpoints return a data array plus a pagination envelope; errors follow the standard {error, message, code, request_id} shape (see error codes).

Search contacts

POST /contacts/search

Faceted, server-side contact search scoped to your organization. The request body holds the filters; pagination is via query params.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
cursorquerystringOpaque pagination cursor from the previous page's pagination.next_cursor.
limitquerystringPage size (numeric string).
categoryquerystringConvenience filter for a single category ID.

Request body

Every field is optional; an empty body matches all contacts in the organization.

FieldTypeRequiredDescription
querystringNoText search across core fields (name, email, company).
custom_field_filtersarrayNoPer custom-field filters: { "name", "value", "type" } where type is one of equal, starts_with, ends_with, contains.
campaign_idsstring[]NoContact must be in ALL of these campaigns.
category_idsstring[]NoContact must have ALL of these categories.
min_campaignsintegerNoMinimum number of associated campaigns.
max_campaignsintegerNoMaximum number of associated campaigns.
subscribedbooleanNoFilter by subscription status.
created_afterstring (RFC 3339)NoCreated on or after this time.
created_beforestring (RFC 3339)NoCreated on or before this time.
updated_afterstring (RFC 3339)NoUpdated on or after this time.
updated_beforestring (RFC 3339)NoUpdated on or before this time.
sort_bystringNoSort column, e.g. first_name, campaign_count.
reversebooleanNoDescending when true.
{
  "query": "acme",
  "subscribed": true,
  "category_ids": ["6f1c0b1e-0c2a-4b3a-9c1e-2d3f4a5b6c7d"],
  "min_campaigns": 1,
  "sort_by": "first_name",
  "reverse": false
}

Response

Returns a data array of contacts plus a pagination envelope.

{
  "data": [
    {
      "id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
      "first_name": "Dana",
      "last_name": "Reyes",
      "email": "[email protected]",
      "company": "Acme",
      "phone": "+15551234567",
      "custom_fields": { "title": "VP Sales" },
      "subscribed": true,
      "campaigns": [{ "id": "c1...", "name": "Q3 Outbound" }],
      "categories": [{ "id": "6f1c...", "title": "VIP", "color": "#0ea5e9" }],
      "verification_status": "valid",
      "verification_reason": "",
      "is_catch_all": false,
      "esp_provider": "gmail",
      "updated_at": "2026-06-10T12:00:00Z",
      "created_at": "2026-05-01T09:30:00Z"
    }
  ],
  "pagination": {
    "total": 1280,
    "next_cursor": "c1_b3BhcXVlLWN1cnNvcg",
    "has_more": true
  }
}

When the search filters by exactly one campaign, each contact additionally carries a campaign_lead object with its processing state inside that campaign (status, sent, opened, clicked, replied, bounced, current_step, last_activity_at).

Create contacts

POST /contacts

Creates one or more contacts. The body is a JSON array, so a single create is an array of length one.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

Request body

A JSON array of contact objects (at least one, up to the per-request maximum; an empty array is a 400).

FieldTypeRequiredDescription
emailstringYesContact email address.
first_namestringNoFirst name.
last_namestringNoLast name.
companystringNoCompany name.
phonestringNoPhone number.
campaignsstring[]NoCampaign IDs to add the contact to.
categoriesstring[]NoCategory IDs to assign.
custom_fieldsobjectNoArbitrary string key/value custom fields.
[
  {
    "email": "[email protected]",
    "first_name": "Lee",
    "last_name": "Ng",
    "company": "Globex",
    "categories": ["6f1c0b1e-0c2a-4b3a-9c1e-2d3f4a5b6c7d"],
    "custom_fields": { "title": "Head of Ops" }
  }
]

Response

Returns the created contacts as a bare JSON array (same contact shape as search).

[
  {
    "id": "2c3d4e5f-6071-4b2c-9d3e-4f5a6b7c8d9e",
    "first_name": "Lee",
    "last_name": "Ng",
    "email": "[email protected]",
    "company": "Globex",
    "phone": "",
    "custom_fields": { "title": "Head of Ops" },
    "subscribed": true,
    "campaigns": [],
    "categories": [{ "id": "6f1c...", "title": "VIP", "color": "#0ea5e9" }],
    "verification_status": "unknown",
    "esp_provider": "",
    "updated_at": "2026-06-11T10:00:00Z",
    "created_at": "2026-06-11T10:00:00Z"
  }
]

Bulk update contacts

PATCH /contacts

Applies one set of edits across a list of contacts: add/remove campaigns and categories, set custom-field operations, and toggle subscription. Up to 1000 contacts per batch.

Auth: Scope BULK_CONTACTS · Org permission manage_contacts

Request body

FieldTypeRequiredDescription
contactsstring[]YesContact IDs to edit (1 to 1000).
add_campaignsstring[]NoCampaign IDs to add.
remove_campaignsstring[]NoCampaign IDs to remove.
add_categoriesstring[]NoCategory IDs to add.
remove_categoriesstring[]NoCategory IDs to remove.
fieldsarrayNoCustom-field operations: { "type", "key", "value" } where type is ADD, EDIT, DELETE, or RENAME.
subscribebooleanNoSet subscription status for all listed contacts.
{
  "contacts": ["1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d", "2c3d4e5f-6071-4b2c-9d3e-4f5a6b7c8d9e"],
  "add_categories": ["6f1c0b1e-0c2a-4b3a-9c1e-2d3f4a5b6c7d"],
  "subscribe": false,
  "fields": [{ "type": "EDIT", "key": "title", "value": "Decision Maker" }]
}

Response

Returns the updated contacts as a bare JSON array (contact shape as above).

[
  {
    "id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
    "email": "[email protected]",
    "subscribed": false,
    "categories": [{ "id": "6f1c...", "title": "VIP", "color": "#0ea5e9" }],
    "updated_at": "2026-06-11T10:05:00Z",
    "created_at": "2026-05-01T09:30:00Z"
  }
]

Bulk delete contacts

DELETE /contacts

Deletes a list of contacts. Up to 1000 IDs per batch.

Auth: Scope BULK_CONTACTS · Org permission manage_contacts

Request body

A JSON array of contact ID strings (1 to 1000; an empty array is a 400).

["1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d", "2c3d4e5f-6071-4b2c-9d3e-4f5a6b7c8d9e"]

Response

204 No Content.

Export contacts

POST /contacts/export

Exports contacts to CSV, XLSX, or JSON. The response is the file itself, not JSON.

Auth: Scope READ_CONTACTS · Org permission view_contacts

Request body

FieldTypeRequiredDescription
formatstringYescsv, xlsx, or json.
scopestringYesall, filtered, or selected.
contact_idsstring[]NoContact IDs when scope is selected.
filtersobjectNoA search-contacts filter body when scope is filtered.
fieldsstring[]NoColumn identifiers in display order (built-ins like email, first_name, or custom:<key>). Empty uses the default columns.
filenamestringNoFilename without extension. Sanitized server-side; empty falls back to contacts-<YYYY-MM-DD>.
{
  "format": "csv",
  "scope": "filtered",
  "filters": { "subscribed": true },
  "fields": ["email", "first_name", "last_name", "company", "custom:title"],
  "filename": "subscribed-contacts"
}

Response

200 OK with the file as an attachment. The relevant headers are:

HeaderDescription
Content-TypeThe export's MIME type (CSV, XLSX, or JSON).
Content-Dispositionattachment; filename="...".
X-Total-RowsNumber of rows written.

Exports are capped at 50,000 rows. Larger sets should be split via filters or selection.

Preview an import

POST /contacts/import/preview

Uploads a CSV or XLSX file and returns detected columns plus a small sample so the client can build a column mapping before committing.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

Send the file as multipart/form-data with a file form field. Uploads are capped at 50 MB.

ParameterInTypeDescription
fileform-datafileThe CSV/XLSX upload.

Response

{
  "filename": "leads.csv",
  "format": "csv",
  "total_rows": 1243,
  "columns": ["Email", "First", "Last", "Company"],
  "has_header": true,
  "sample_rows": [
    ["[email protected]", "Dana", "Reyes", "Acme"]
  ],
  "suggested_mapping": [
    { "index": 0, "target": "email" },
    { "index": 1, "target": "first_name" },
    { "index": 2, "target": "last_name" },
    { "index": 3, "target": "company" }
  ]
}

sample_rows is capped at 20 rows. suggested_mapping is a default the client may override.

Commit an import

POST /contacts/import/commit

Re-uploads the file with a mapping and dedup options, applies it, and returns per-row results.

Auth: Scope BULK_CONTACTS · Org permission manage_contacts

Send multipart/form-data with a file field and an options field containing the JSON below as a string.

ParameterInTypeDescription
fileform-datafileThe CSV/XLSX upload (max 50 MB).
optionsform-datastringJSON-encoded commit options (below).

options fields

FieldTypeRequiredDescription
mappingarrayYesColumn mappings: { "index", "target", "custom_key" }. target is ignore, email, first_name, last_name, company, phone, subscribed, categories, or custom:<key>.
dedupstringYesskip, update, or create_duplicate for rows whose email matches an existing contact.
has_headerbooleanYesWhether the first row is a header.
category_idsstring[]NoCategories to assign to imported contacts.
campaign_idsstring[]NoCampaigns to add imported contacts to.
subscribed_defaultbooleanNoSubscription state for new contacts when no subscribed column is mapped. Defaults to true.
{
  "mapping": [
    { "index": 0, "target": "email" },
    { "index": 1, "target": "first_name" },
    { "index": 3, "target": "custom:company_size", "custom_key": "company_size" }
  ],
  "dedup": "update",
  "has_header": true,
  "category_ids": ["6f1c0b1e-0c2a-4b3a-9c1e-2d3f4a5b6c7d"],
  "subscribed_default": true
}

Response

{
  "total": 1243,
  "imported": 1180,
  "updated": 41,
  "skipped": 18,
  "failed": 4,
  "started_at": "2026-06-11T10:10:00Z",
  "ended_at": "2026-06-11T10:10:07Z",
  "errors": [
    { "line": 57, "email": "not-an-email", "reason": "invalid email" }
  ]
}

Imports are capped at 50,000 rows.

Look up a contact by email

GET /contacts/lookup

Resolves a sender address to a contact in your organization. Returns 200 with {"contact": null} when nothing matches, so unknown senders render a clean empty state rather than a 404. A display-name wrapped address (Name <[email protected]>) is accepted and unwrapped.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
emailquerystringThe email address to resolve (required).

Response

{
  "contact": {
    "id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
    "first_name": "Dana",
    "last_name": "Reyes",
    "email": "[email protected]",
    "company": "Acme",
    "subscribed": true,
    "campaigns": [],
    "categories": [],
    "verification_status": "valid",
    "esp_provider": "gmail",
    "updated_at": "2026-06-10T12:00:00Z",
    "created_at": "2026-05-01T09:30:00Z"
  }
}

Get a contact

GET /contacts/:id

Returns the hydrated contact 360 payload: the contact plus an engagement summary and, when present, suppression state. Engagement and suppression counts are org-scoped; they are returned only when an organization is selected.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.

Response

{
  "id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
  "first_name": "Dana",
  "last_name": "Reyes",
  "email": "[email protected]",
  "company": "Acme",
  "phone": "+15551234567",
  "custom_fields": { "title": "VP Sales" },
  "subscribed": true,
  "campaigns": [{ "id": "c1...", "name": "Q3 Outbound" }],
  "categories": [{ "id": "6f1c...", "title": "VIP", "color": "#0ea5e9" }],
  "verification_status": "valid",
  "esp_provider": "gmail",
  "updated_at": "2026-06-10T12:00:00Z",
  "created_at": "2026-05-01T09:30:00Z",
  "engagement": {
    "total_sent": 4,
    "total_opened": 3,
    "total_clicked": 1,
    "total_replied": 1,
    "total_bounced": 0,
    "total_complained": 0,
    "last_sent_at": "2026-06-09T08:00:00Z",
    "last_opened_at": "2026-06-09T08:14:00Z",
    "last_replied_at": "2026-06-09T11:02:00Z"
  },
  "suppression": null
}

When the contact is suppressed, suppression is an object: { "reason", "source", "expires_at", "created_at" } where source is bounce, complaint, or unsubscribe.

Update a contact

PATCH /contacts/:id

Partially updates a single contact. Only the fields present are changed. Campaign and category lists can be set wholesale or adjusted with diff-style add/remove.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

ParameterInTypeDescription
idpathUUIDContact ID.

Request body

FieldTypeRequiredDescription
first_namestringNoFirst name.
last_namestringNoLast name.
companystringNoCompany.
phonestringNoPhone.
custom_fieldsobjectNoReplaces the custom-fields map.
subscribedbooleanNoSubscription status.
campaignsstring[]NoSet the full campaign membership (nil leaves as-is).
categoriesstring[]NoSet the full category list (nil leaves as-is).
add_categoriesstring[]NoDiff-style add (ignored when categories is set).
remove_categoriesstring[]NoDiff-style remove (ignored when categories is set).
{
  "company": "Acme Corp",
  "subscribed": true,
  "add_categories": ["6f1c0b1e-0c2a-4b3a-9c1e-2d3f4a5b6c7d"]
}

Response

Returns the updated contact as a bare object (contact shape as in search).

{
  "id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
  "first_name": "Dana",
  "last_name": "Reyes",
  "email": "[email protected]",
  "company": "Acme Corp",
  "subscribed": true,
  "campaigns": [],
  "categories": [{ "id": "6f1c...", "title": "VIP", "color": "#0ea5e9" }],
  "updated_at": "2026-06-11T10:20:00Z",
  "created_at": "2026-05-01T09:30:00Z"
}

Delete a contact

DELETE /contacts/:id

Deletes a single contact.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

ParameterInTypeDescription
idpathUUIDContact ID.

Response

204 No Content.

List emails sent to a contact

GET /contacts/:id/emails

Returns one row per email sent (or attempted) to the contact, newest first, with sender, campaign, sequence, and engagement timestamps. Pagination is keyed on the (created_at, task_id) of the last row.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
limitqueryintegerPage size, 1 to 200 (default 50).
before_atquerystring (RFC 3339 nano)created_at of the last row from the previous page.
before_idqueryUUIDtask_id of the last row from the previous page.

Both before_at and before_id must be supplied together; otherwise the cursor is ignored and the first page is returned.

Response

Returns a data array plus a pagination envelope.

{
  "data": [
    {
      "task_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "status": "sent",
      "message_id": "<[email protected]>",
      "subject": "Quick question",
      "sent_at": "2026-06-09T08:00:00Z",
      "email_account_id": "e1...",
      "email_account_email": "[email protected]",
      "email_account_name": "Rep One",
      "campaign_id": "c1...",
      "campaign_name": "Q3 Outbound",
      "step_id": "s1...",
      "step_name": "Email 1",
      "opened_at": "2026-06-09T08:14:00Z",
      "replied_at": "2026-06-09T11:02:00Z"
    }
  ],
  "pagination": {
    "total": 4,
    "next_cursor": null,
    "has_more": false
  }
}

List a contact's timeline

GET /contacts/:id/timeline

Returns the merged activity feed for a contact: sends, opens, clicks, replies, bounces, deliverability and suppression events, notes, and meeting bookings. Requires a selected organization (org-scoped events would otherwise be hidden), so a request with no organization returns 400.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
limitqueryintegerPage size, 1 to 200 (default 50).
beforequerystring (RFC 3339 nano)The at timestamp of the oldest event from the previous page.

Response

Returns a data array and a has_more flag (not a cursor envelope). Paginate by passing the oldest event's at as before.

{
  "data": [
    {
      "type": "email_replied",
      "at": "2026-06-09T11:02:00Z",
      "email_account_id": "e1...",
      "email_account_email": "[email protected]",
      "campaign_id": "c1...",
      "campaign_name": "Q3 Outbound",
      "task_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "subject": "Quick question"
    },
    {
      "type": "note",
      "at": "2026-06-08T16:30:00Z",
      "content": "Met at the conference, wants a follow-up in July.",
      "user_id": "u1..."
    }
  ],
  "has_more": false
}

type is one of email_sent, email_opened, email_clicked, email_replied, email_bounced, reply_received, deliverability, suppressed, note, meeting_booked, meeting_rescheduled, or meeting_canceled. Fields not relevant to an event type are omitted.

List a contact's activities

GET /contacts/:id/activities

Returns the structured CRM activity log for a contact (note, deal, task, campaign, and engagement events recorded in the CRM activity table). Requires a selected organization.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
limitqueryintegerPage size, 1 to 100 (default 50).
cursorqueryUUIDOpaque cursor from the previous page's pagination.next_cursor.

Response

{
  "data": [
    {
      "id": "ac1...",
      "contact_id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
      "organization_id": "org1...",
      "user_id": "u1...",
      "activity_type": "note_added",
      "metadata": { "note_id": "n1..." },
      "created_at": "2026-06-08T16:30:00Z"
    }
  ],
  "pagination": {
    "total": 12,
    "next_cursor": "ac0...",
    "has_more": true
  }
}

activity_type is a closed enum including email_sent, email_opened, email_clicked, email_replied, email_bounced, note_added, note_updated, deal_created, deal_stage_changed, deal_won, deal_lost, task_created, task_completed, contact_created, contact_updated, campaign_added, and campaign_removed.

List a contact's notes

GET /contacts/:id/notes

Returns the CRM notes attached to a contact, newest first. Requires a selected organization.

Auth: Scope READ_CONTACTS · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
limitqueryintegerPage size, 1 to 100 (default 50).
cursorqueryUUIDOpaque cursor from the previous page's pagination.next_cursor.

Response

{
  "data": [
    {
      "id": "n1b2c3d4-5f60-4a1b-8c2d-3e4f5a6b7c8d",
      "contact_id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
      "organization_id": "org1...",
      "user_id": "u1...",
      "content": "Met at the conference, wants a follow-up in July.",
      "created_at": "2026-06-08T16:30:00Z",
      "updated_at": "2026-06-08T16:30:00Z",
      "user": { "id": "u1...", "name": "Sam Rep" }
    }
  ],
  "pagination": {
    "total": 3,
    "next_cursor": null,
    "has_more": false
  }
}

Create a contact note

POST /contacts/:id/notes

Adds a note to a contact. Requires a selected organization.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

ParameterInTypeDescription
idpathUUIDContact ID.

Request body

FieldTypeRequiredDescription
contentstringYesNote body (1 to 10,000 characters).
{ "content": "Sent the proposal, following up Monday." }

Response

201 Created with the created note (same shape as a note in the list).

{
  "id": "n2c3d4e5-6071-4b2c-9d3e-4f5a6b7c8d9e",
  "contact_id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
  "organization_id": "org1...",
  "user_id": "u1...",
  "content": "Sent the proposal, following up Monday.",
  "created_at": "2026-06-11T10:30:00Z",
  "updated_at": "2026-06-11T10:30:00Z"
}

Update a contact note

PATCH /contacts/:id/notes/:noteId

Edits a note's content. Requires a selected organization.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
noteIdpathUUIDNote ID.

Request body

FieldTypeRequiredDescription
contentstringNoNew note body.
{ "content": "Sent the proposal, following up Tuesday." }

Response

Returns the updated note.

{
  "id": "n2c3d4e5-6071-4b2c-9d3e-4f5a6b7c8d9e",
  "contact_id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
  "organization_id": "org1...",
  "user_id": "u1...",
  "content": "Sent the proposal, following up Tuesday.",
  "created_at": "2026-06-11T10:30:00Z",
  "updated_at": "2026-06-11T10:35:00Z"
}

Delete a contact note

DELETE /contacts/:id/notes/:noteId

Deletes a note. Requires a selected organization.

Auth: Scope WRITE_CONTACTS · Org permission manage_contacts

ParameterInTypeDescription
idpathUUIDContact ID.
noteIdpathUUIDNote ID.

Response

204 No Content.

List a contact's deals

GET /contacts/:id/deals

Returns the CRM deals associated with a contact as a bare JSON array.

Auth: Scope READ_CRM · Org permission view_contacts

ParameterInTypeDescription
idpathUUIDContact ID.

Response

[
  {
    "id": "d1b2c3d4-5f60-4a1b-8c2d-3e4f5a6b7c8d",
    "organization_id": "org1...",
    "pipeline_id": "p1...",
    "stage_id": "st1...",
    "contact_id": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d",
    "name": "Acme expansion",
    "value": 12000,
    "currency": "USD",
    "status": "open",
    "expected_close_date": "2026-07-15T00:00:00Z",
    "campaign_id": "c1...",
    "created_at": "2026-06-05T09:00:00Z",
    "updated_at": "2026-06-10T12:00:00Z"
  }
]

status is open, won, or lost. value, expected_close_date, won_at, lost_at, lost_reason, assigned_to, campaign_id, and source_mailbox_id are nullable and omitted when unset.

On this page