API v2 Contacts and Leads

List contacts and leads, upsert one contact by external ID, and bulk import contacts for an agent.

Contacts associate your own stable end-user IDs with profile fields and private custom attributes. Leads are read-only submissions captured by an agent. Every endpoint on this page requires Authorization: Bearer YOUR_API_KEY and access to {agentId}.

For signed identity claims in embedded chat, see Identity Verification. See the error catalog for shared authentication and validation errors.

Contact Object

FieldTypeNotes
idstringContact UUID.
externalIdstringYour stable end-user ID.
namestring or nullContact name.
emailstring or nullContact email address.
phonestring or nullContact phone number.
customAttributesobject or nullPrivate JSON attributes stored on the contact.
createdAtinteger or nullUnix timestamp in seconds.
updatedAtinteger or nullUnix timestamp in seconds.
lastSeenAtinteger or nullUnix timestamp in seconds.

List Contacts

Path: /api/v2/agents/{agentId}/contacts

GET /api/v2/agents/{agentId}/contacts

Authentication: Bearer API key with access to {agentId}.

QueryRequiredConstraints
limitNoInteger from 1 to 100; defaults to 20.
cursorNoOpaque cursor returned by the preceding page. Replay it unchanged.
searchNoTrimmed, case-insensitive substring search (ILIKE) across external ID, email, name, and phone. Percent signs (%) are stripped, but the underscore (_) is not escaped and acts as a single-character wildcard — e.g. acct_1 also matches acctX1. An empty result after trimming is ignored.
curl 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/contacts?search=alice&limit=20' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Success: 200 OK

{
  "data": [
    {
      "id": "f9878f31-c2b3-469f-a82e-26e996e67721",
      "externalId": "customer_123",
      "name": "Alice Example",
      "email": "[email protected]",
      "phone": "+1 555 0100",
      "customAttributes": { "plan": "pro", "seats": 4 },
      "createdAt": 1784332800,
      "updatedAt": 1784332800,
      "lastSeenAt": null
    }
  ],
  "pagination": {
    "cursor": null,
    "hasMore": false,
    "total": 1
  }
}

Invalid limits or cursors return 400 VALIDATION_INVALID_BODY.

Lead Object

FieldTypeNotes
idstringLead UUID.
namestring or nullSubmitted name.
emailstring or nullSubmitted email address.
phonestring or nullSubmitted phone number.
conversationIdstring or nullPublic conversation reference ID when the lead is linked to a conversation.
createdAtintegerUnix timestamp in seconds.

List Leads

Path: /api/v2/agents/{agentId}/leads

GET /api/v2/agents/{agentId}/leads

Authentication: Bearer API key with access to {agentId}.

QueryRequiredConstraints
limitNoInteger from 1 to 100; defaults to 20.
cursorNoOpaque cursor returned by the preceding page. Replay it unchanged.
createdAfterNoISO 8601 datetime with Z or a UTC offset. The lower bound is inclusive.
createdBeforeNoISO 8601 datetime with Z or a UTC offset. The upper bound is inclusive.

When both date filters are present, createdAfter must be less than or equal to createdBefore when compared as instants. Equal bounds are valid.

curl 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/leads?createdAfter=2026-07-18T08:00:00Z&limit=20' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Success: 200 OK

{
  "data": [
    {
      "id": "ad47f673-6bfb-43a0-a296-d79c84870a32",
      "name": "Alice Example",
      "email": "[email protected]",
      "phone": "+1 555 0100",
      "conversationId": "a1b2c3d4e5f6g7h8",
      "createdAt": 1784361600
    }
  ],
  "pagination": {
    "cursor": null,
    "hasMore": false,
    "total": 1
  }
}

Leads are ordered by submission time from newest to oldest. Invalid limits, cursors, datetimes, or inverted date bounds return 400 VALIDATION_INVALID_BODY.

Create or Update One Contact

POST /api/v2/agents/{agentId}/contacts

Authentication: Bearer API key with access to {agentId}.

This endpoint upserts on the pair {agentId, externalId}. When a matching contact exists, only profile fields present in the request are updated. An omitted field preserves its stored value; an explicit null or empty string clears it.

Body fieldRequiredType and constraints
externalIdYesString, trimmed, 1 to 255 characters.
nameNoString trimmed to at most 255 characters, or null.
emailNoValid email address trimmed to at most 320 characters, an empty string, or null.
phoneNoString trimmed to at most 50 characters, an empty string, or null.
customAttributesNoObject containing JSON values. Omit it to preserve stored attributes.
curl -X POST 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/contacts' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"externalId":"customer_123","name":"Alice Example","email":"[email protected]","customAttributes":{"plan":"pro","seats":4}}'

Success: 201 Created for both inserts and conflict updates. The contact object is returned bare.

{
  "id": "f9878f31-c2b3-469f-a82e-26e996e67721",
  "externalId": "customer_123",
  "name": "Alice Example",
  "email": "[email protected]",
  "phone": null,
  "customAttributes": { "plan": "pro", "seats": 4 },
  "createdAt": 1784332800,
  "updatedAt": 1784332800,
  "lastSeenAt": null
}

Invalid bodies return 400 VALIDATION_INVALID_BODY with validation details.

Import Contacts

Path: /api/v2/agents/{agentId}/contacts/import

POST /api/v2/agents/{agentId}/contacts/import

Authentication: Bearer API key with access to {agentId}.

The request uses snake_case field names. rows must contain 1 to 1,000 entries.

Row fieldRequiredType and constraints
external_idYesString, trimmed, 1 to 255 characters.
nameNoString trimmed to at most 255 characters, or null.
emailNoValid email address trimmed to at most 320 characters, an empty string, or null.
phoneNoString trimmed to at most 50 characters, or null; an empty string is also accepted and stored as null.
customAttributesNoObject containing JSON values. Omit it to preserve stored attributes for that row.

Rows are deduplicated by external_id within the request; the last occurrence wins. Unlike the single-contact partial upsert, the import constructs every profile field for each retained row, so an omitted, null, or empty profile value is stored as null on conflict.

curl -X POST 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/contacts/import' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"rows":[{"external_id":"customer_123","name":"Alice Example","email":"[email protected]","phone":"+1 555 0100","customAttributes":{"plan":"pro"}},{"external_id":"customer_456","name":"Bob Example","email":"[email protected]","phone":null}]}'

Success: 200 OK

{
  "processed": 2,
  "skipped": 0
}

processed is the number of unique external IDs upserted. skipped is the number of duplicate rows removed from the submitted batch. Invalid bodies, including empty batches and batches over 1,000 rows, return 400 VALIDATION_INVALID_BODY.