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.
404, not 403— we don't reveal whether a resource exists in another tenant.Look up by phone
/v1/contacts/by-phoneScope: contacts:read
Reverse-lookup a contact and its tag assignments without holding the contact ID. Phone normalization (+, spaces, - stripped) happens server-side.
| Name | Type | Required | Description |
|---|---|---|---|
| phone | string | yes | Recipient 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
/v1/contacts/:contactIdScope: contacts:read
Example
curl https://api.sigfollow.com/v1/contacts/cm5xy123 \
-H "Authorization: Bearer sflo_live_xxx"Create or upsert
/v1/contactsScope: 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.
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
| Name | Type | Required | Description |
|---|---|---|---|
| phone | string | yes | Recipient phone. 5–32 chars after normalization. |
| display_name | string | null | no | Display 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
/v1/contacts/:contactIdScope: 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.
| Name | Type | Required | Description |
|---|---|---|---|
| display_name | string | null | no | Pass 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
/v1/contacts/:contactIdScope: 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
/v1/contacts/:contactId/tagsScope: 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
| Name | Type | Required | Description |
|---|---|---|---|
| tag_id | string (CUID) | yes | ID 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. |
| reason | string | no | Free-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"
}'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
/v1/contacts/:contactId/tags/:tagIdScope: 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"