SigFollow
Sign in

API reference

Contacts

Sync customer contacts from your CRM and maintain their tag assignments. Contacts are tenant-scoped — every endpoint operates on the API key's tenant only.

Resource model. A Contact is the long-lived anchor for a single WhatsApp end-user inside a tenant. It holds the normalized phone number, an optional display name, and the join points for ContactTagAssignment records. Inbound messages also lazy-create Contact rows; the Public API lets you preempt that with explicit creates.

Tenant isolation
Every endpoint here uses the API key's tenant. There is no path parameter for tenant ID, and queries cannot inject one. Cross-tenant access returns 404, not 403— we don't reveal whether a resource exists in another tenant.

Look up by phone

GET/v1/contacts/by-phone

Scope: contacts:read

Reverse-lookup a contact and its tag assignments without holding the contact ID. Phone normalization (+, spaces, - stripped) happens server-side.

Query
NameTypeRequiredDescription
phonestringyesRecipient phone, normalization tolerant.

Example

curl -G https://api.sigfollow.com/v1/contacts/by-phone \
  -H "Authorization: Bearer sflo_live_xxx" \
  --data-urlencode "phone={{Recipient-Phone-Number}}"

Returns a Contact object with its tag assignments. Returns 404 if the phone is not in this tenant.

Get by ID

GET/v1/contacts/:contactId

Scope: contacts:read

Example

curl https://api.sigfollow.com/v1/contacts/cm5xy123 \
  -H "Authorization: Bearer sflo_live_xxx"

Create or upsert

POST/v1/contacts

Scope: contacts:write

Idempotent on (tenantId, phone). New contact → row created with firstSeenAt = lastSeenAt = now. Existing contact → lastSeenAt refreshed; display_name overwritten only when non-null.

Not subject to inbound cooldown
Inbound messages lazy-upsert Contactrows behind a 60s Redis SETNX cooldown to protect the hot path. The Public API write path bypasses that cooldown — every request actually hits the database. This is by design: explicit API integrations are low-frequency and shouldn't be silently no-op'd.

Body

NameTypeRequiredDescription
phonestringyesRecipient phone. 5–32 chars after normalization.
display_namestring | nullnoDisplay name. Trimmed and capped at 128 characters. Pass null or omit to leave the existing value untouched (upsert semantics).

Example

curl -X POST https://api.sigfollow.com/v1/contacts \
  -H "Authorization: Bearer sflo_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "{{Recipient-Phone-Number}}",
    "display_name": "Alice Liu"
  }'

Response

{
  "id": "cm5xy123abcdef",
  "tenantId": "cm5xy000tenant",
  "phone": "{{Recipient-Phone-Number}}",
  "displayName": "Alice Liu",
  "firstSeenAt": "2026-05-13T22:11:00.000Z",
  "lastSeenAt": "2026-05-13T22:30:00.000Z",
  "createdAt": "2026-05-13T22:11:00.000Z",
  "updatedAt": "2026-05-13T22:30:00.000Z"
}

Update display name

PATCH/v1/contacts/:contactId

Scope: contacts:write

Only display_nameis mutable. The phone field is the contact's identity anchor ((tenantId, phone) unique key) — to change a phone, delete the old contact and create a new one.

NameTypeRequiredDescription
display_namestring | nullnoPass a string to update, null to clear, or omit the field for a no-op (PATCH returns current state).

Example

curl -X PATCH https://api.sigfollow.com/v1/contacts/cm5xy123 \
  -H "Authorization: Bearer sflo_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "display_name": "Alice L." }'

Delete

DELETE/v1/contacts/:contactId

Scope: contacts:write

Hard delete. All ContactTagAssignment rows cascade with the contact. 404when the contact doesn't exist or belongs to another tenant.

Example

curl -X DELETE https://api.sigfollow.com/v1/contacts/cm5xy123 \
  -H "Authorization: Bearer sflo_live_xxx"
{ "ok": true }

Assign a tag

POST/v1/contacts/:contactId/tags

Scope: contacts:write

Source is hard-coded to API. SigFollow's source priority table (MANUAL_ADMIN = API > MANUAL_AGENT > AI_TOOL > AI_AUTO_CLOSE) means an API-applied tag can override agent/AI tags but is equal-weight with admin-applied tags.

Body

NameTypeRequiredDescription
tag_idstring (CUID)yesID of the tag in the tenant's tag catalog. The tag must exist and not be soft-deleted. Find IDs via the admin Audiences / Tags page.
reasonstringnoFree-form audit note, max 500 chars. Useful for explaining where the tag came from (e.g. CRM list name).

Example

curl -X POST https://api.sigfollow.com/v1/contacts/cm5xy123/tags \
  -H "Authorization: Bearer sflo_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "tag_id": "cm5tag42abcdef",
    "reason": "Imported from Salesforce VIP list"
  }'
Tag limit per contact
Each contact can hold at most 20 active assignments. Hitting the cap returns 409 with Meta error code 131000. Remove an existing tag first, or open the admin Tags page to clean up the contact's assignments.

Remove a tag

DELETE/v1/contacts/:contactId/tags/:tagId

Scope: contacts:write

Idempotent — a non-existent (contact, tag) pair returns 200 with { "ok": true } (no audit row is kept; consult server logs for the operation history).

Example

curl -X DELETE \
  https://api.sigfollow.com/v1/contacts/cm5xy123/tags/cm5tag42 \
  -H "Authorization: Bearer sflo_live_xxx"