CRM API Reference

Complete API reference for all CRM endpoints — contacts, companies, activities, tags, lists, properties, and CQL queries.

CRM API Reference

All CRM endpoints require a Server API key with CRM scope.

Authorization: Bearer YOUR_SERVER_API_KEY

Write operations use the Kafka-first pattern and return 202 Accepted with a provisional record and X-Correlation-Id header. The actual DB write happens asynchronously.

Base URL: https://api.usetransactional.com/crm


Contacts

List contacts

GET /crm/contacts

Query parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
searchstringSearch by name, email, phone
statusstringFilter: ACTIVE, ARCHIVED, DELETED
lifecycleStagestringFilter: SUBSCRIBER, LEAD, MARKETING_QUALIFIED, SALES_QUALIFIED, OPPORTUNITY, CUSTOMER, EVANGELIST
modulestringFilter by active module
isIdentifiedbooleanFilter identified contacts
fromstringCreated after (ISO date)
tostringCreated before (ISO date)
sortBystringcreatedAt, lastActivityAt, email, name
sortOrderstringdescasc or desc

Response:

{
  "data": [{ "id": 1, "email": "alice@example.com", "name": "Alice", ... }],
  "meta": { "page": 1, "limit": 20, "total": 42, "totalPages": 3 }
}

Search contacts

GET /crm/contacts/search?q=alice&limit=20

Typeahead search across name, email, and phone.


Filter contacts (CQL)

POST /crm/contacts/filter
{
  "filter": {
    "logic": "AND",
    "conditions": [
      { "type": "field", "field": "status", "operator": "eq", "value": "ACTIVE" },
      { "type": "field", "field": "lifecycleStage", "operator": "in", "value": ["LEAD", "CUSTOMER"] }
    ]
  },
  "page": 1,
  "limit": 20,
  "sortBy": "createdAt",
  "sortOrder": "desc"
}

Get contact

GET /crm/contacts/:id

Accepts numeric ID, external ID (ctc_*), or email address.

Returns the full contact with companies, tags, custom properties, and activity count.


Create contact

POST /crm/contacts
{
  "email": "alice@example.com",
  "name": "Alice Johnson",
  "givenName": "Alice",
  "familyName": "Johnson",
  "phone": "+1234567890",
  "lifecycleStage": "LEAD",
  "properties": {
    "mrr": 4999,
    "plan": "Enterprise"
  }
}

Response: 202 Accepted

{
  "data": {
    "id": null,
    "externalId": "ctc_abc123...",
    "email": "alice@example.com",
    "status": "ACTIVE",
    "pending": true
  }
}

Headers: X-Correlation-Id, Location: /crm/contacts/ctc_abc123...

Dedup: Returns 409 if a contact with the same email already exists.


Update contact

PATCH /crm/contacts/:id
{
  "name": "Alice Smith",
  "lifecycleStage": "CUSTOMER",
  "properties": { "mrr": 9999 }
}

Response: 202 Accepted with merged provisional record.


Delete contact

DELETE /crm/contacts/:id

Soft-delete (sets status to DELETED). Response: 202 Accepted.


Merge contacts

POST /crm/contacts/:winnerId/merge
{ "loserId": 2 }

The winner absorbs the loser's companies, tags, activities. Response: 202 Accepted.


Assign tag

POST /crm/contacts/:id/tags
{ "tagId": 5 }

Remove tag

DELETE /crm/contacts/:id/tags/:tagId

List activities

GET /crm/contacts/:id/activities
ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
typestringFilter by type: NOTE, CALL, MEETING, EMAIL, TASK, PAGE_VIEW, CUSTOM_EVENT
modulestringFilter by module: CRM, SUPPORT, INTEGRATION
fromstringAfter (ISO date)
tostringBefore (ISO date)

Log activity

POST /crm/contacts/:id/activities
{
  "type": "CALL",
  "content": "Discussed pricing and timeline",
  "properties": { "duration": "30m", "outcome": "interested" }
}

Activity types: NOTE, CALL, MEETING, EMAIL, TASK

Response: 202 Accepted. Activities are pushed to connected integrations (HubSpot as native engagements, Mailchimp as notes + events).


List associated companies

GET /crm/contacts/:id/companies

Returns companies linked to this contact with role and primary flag.


Associate company

POST /crm/contacts/:id/companies
{ "companyId": 5, "role": "Decision Maker", "isPrimary": true }

Remove company association

DELETE /crm/contacts/:id/companies/:companyId

Sync status

GET /crm/contacts/:id/sync-status

Returns integration sync state for this contact: external IDs, provider names, last synced timestamps.

{
  "data": {
    "contactId": 1,
    "externalId": "ctc_abc123...",
    "integrations": [
      {
        "integrationId": 2,
        "integrationName": "HubSpot - Production",
        "provider": "HUBSPOT",
        "externalId": "469194699512",
        "lastSyncedAt": "2026-04-10T10:30:36Z",
        "syncHash": "a1b2c3..."
      }
    ]
  }
}

Contact summary

GET /crm/contacts/:id/summary

One-view summary designed for AI agents and quick lookups.

{
  "data": {
    "id": 1,
    "name": "Alice Johnson",
    "email": "alice@example.com",
    "status": "ACTIVE",
    "lifecycleStage": "CUSTOMER",
    "companies": [{ "name": "Acme Corp", "domain": "acme.com", "role": "CTO" }],
    "tags": ["vip", "enterprise"],
    "activityCount": 26,
    "lastActivityAt": "2026-04-10T10:30:00Z"
  }
}

Companies

List companies

GET /crm/companies

Same query parameters as contacts: page, limit, search, status, size, industry, from, to, sortBy, sortOrder.


Search companies

GET /crm/companies/search?q=acme&limit=20

Filter companies (CQL)

POST /crm/companies/filter

Same format as contacts filter.


Get company

GET /crm/companies/:id

Accepts numeric ID or external ID (cmp_*).


Create company

POST /crm/companies
{
  "name": "Acme Corp",
  "domain": "acme.com",
  "industry": "SaaS",
  "size": "50-200",
  "plan": "Enterprise",
  "website": "https://acme.com",
  "phone": "+1234567890",
  "properties": { "annual_revenue": "5000000" }
}

Dedup: Returns 409 if a company with the same domain already exists.


Update company

PATCH /crm/companies/:id

Delete company

DELETE /crm/companies/:id

List contacts for company

GET /crm/companies/:id/contacts

Returns all contacts linked to this company with roles.


Tags

List tags

GET /crm/tags

Create tag

POST /crm/tags
{
  "key": "vip",
  "name": "VIP Customer",
  "description": "High-value customers",
  "color": "#FFD700"
}

Key must be lowercase alphanumeric with hyphens or underscores. Dedup: Returns 409 if key already exists.


Update tag

PATCH /crm/tags/:id
{ "name": "Enterprise VIP", "color": "#FF6600" }

Delete tag

DELETE /crm/tags/:id

Lists / Segments

List all lists

GET /crm/lists?entityType=CONTACT

Get list

GET /crm/lists/:id

Create list

POST /crm/lists
{
  "name": "Active Customers",
  "entityType": "CONTACT",
  "isDynamic": true,
  "filterCriteria": {
    "logic": "AND",
    "conditions": [
      { "type": "field", "field": "lifecycleStage", "operator": "eq", "value": "CUSTOMER" }
    ]
  }
}

Update list

PATCH /crm/lists/:id

Delete list

DELETE /crm/lists/:id

Evaluate dynamic list

POST /crm/lists/:id/evaluate

Re-runs the CQL filter and updates membership. Returns the updated member count.


List members

GET /crm/lists/:id/members?page=1&limit=50

Add members

POST /crm/lists/:id/members
{ "entityIds": [1, 2, 3] }

Static lists only. Max 1000 per call.


Remove members

DELETE /crm/lists/:id/members
{ "entityIds": [4, 5] }

Properties

List property definitions

GET /crm/properties?entity=CONTACT&group=CUSTOM
ParameterTypeDescription
entitystringCONTACT, COMPANY, CONTACT_COMPANY
groupstringBASIC, CONTACT_INFO, SOCIAL, COMPANY_INFO, CUSTOM
isSystembooleanFilter system vs custom properties

Create property

POST /crm/properties
{
  "key": "mrr",
  "entity": "CONTACT",
  "label": "Monthly Revenue",
  "type": "NUMBER",
  "group": "CUSTOM",
  "description": "Monthly recurring revenue in USD",
  "isRequired": false
}

Property types: TEXT, NUMBER, BOOLEAN, DATE, DATETIME, EMAIL, PHONE, URL, SELECT, MULTI_SELECT, CURRENCY, TEXTAREA

Dedup: Returns 409 if (entity, key) already exists.


Update property

PATCH /crm/properties/:id

Archive property

DELETE /crm/properties/:id

Soft-archive — values are preserved, field is hidden from the UI.


CQL Query

Execute query

POST /crm/query
{
  "entity": "contact",
  "where": "status = ACTIVE AND email contains '@example.com'",
  "sort": "createdAt:desc",
  "page": 1,
  "limit": 20
}

Alternatively use the JSON filter format:

{
  "entity": "contact",
  "filter": {
    "logic": "AND",
    "conditions": [
      { "type": "field", "field": "status", "operator": "eq", "value": "ACTIVE" }
    ]
  }
}

Response:

{
  "data": [...],
  "meta": {
    "total": 142,
    "limit": 20,
    "page": 1,
    "totalPages": 8,
    "queryTimeMs": 12,
    "estimatedCost": 5
  }
}

List queryable fields

GET /crm/fields?entity=contact

Returns all system fields and custom properties with their types, operators, and access levels.


Legacy Endpoints

These endpoints still work for backward compatibility but the RESTful versions above are preferred:

LegacyRESTful
GET /crm/activities/contacts/:idGET /crm/contacts/:id/activities
POST /crm/activities/contacts/:idPOST /crm/contacts/:id/activities

Error Codes

CodeHTTPDescription
NOT_FOUND404Contact, company, tag, list, or property not found
INVALID_ID400ID parameter is not a valid number or external ID
CONTACT_EMAIL_EXISTS409A contact with this email already exists
COMPANY_DOMAIN_EXISTS409A company with this domain already exists
TAG_KEY_EXISTS409A tag with this key already exists
PROPERTY_KEY_EXISTS409A property with this key+entity already exists
ALREADY_ASSOCIATED409Contact is already associated with the company
INVALID_REQUEST400Cannot merge with self, cannot add to dynamic list, etc.
PENDING202Resource exists but write hasn't committed yet (Kafka-first)

Rate Limits

CRM endpoints share the server API key's rate limit tier. Write operations are processed asynchronously via Kafka and don't block the HTTP response.

Integration sync to external providers (HubSpot, Mailchimp) is separately rate-limited:

  • HubSpot: 80 requests / 10 seconds
  • Mailchimp: 8 requests / second