Contacts API

The Contacts API follows the same conventions as Leads API — paginated lists, JSON bodies, JWT auth, tenant-scoped automatically.

List

GET /api/contacts/?account=<account_id>&search=jane
Authorization: Bearer <token>
{
  "count": 84,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "c012-…",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane@acme.com",
      "phone": "+1 555 1234",
      "title": "VP Engineering",
      "account": { "id": "a001-…", "name": "Acme Corp" },
      "linkedin_url": "https://linkedin.com/in/janedoe",
      "owner": { "id": "u034-…", "email": "rep@yours.com" },
      "custom_fields": { "preferred_channel": "email" },
      "created_at": "2026-02-14T09:22:01Z"
    }
  ]
}

Filterable fields: account, owner, search (matches name, email, phone), tags, created_at__gte, cf_<key>.

Create

POST /api/contacts/
Content-Type: application/json
Authorization: Bearer <token>

{
  "first_name": "Jane",
  "last_name": "Doe",
  "email": "jane@acme.com",
  "phone": "+1 555 1234",
  "title": "VP Engineering",
  "account_id": "a001-…",
  "custom_fields": { "preferred_channel": "email" }
}

account_id is optional — pass null for standalone contacts.

Update / delete

PATCH  /api/contacts/<id>/   # partial
PUT    /api/contacts/<id>/   # full
DELETE /api/contacts/<id>/   # soft delete
DELETE /api/contacts/<id>/?forget=true   # hard delete + PII scrub

Merge

POST /api/contacts/<id>/merge/
{ "loser_id": "c099-…" }

The loser's emails, calls, and references migrate to the winner inside a transaction. The winner's own fields take precedence on conflicts.

CSV upload

POST /api/contacts/upload/
Content-Type: multipart/form-data

file=<contacts.csv>

The response is the same per-row shape as the Leads upload — created, updated, skipped, errors.