Contacts
Contacts are shared workspace profiles. Reading lists or details requires contacts:read; creating, editing, archiving, restoring or importing profiles requires contacts:write. All mutations carry Idempotency-Key.
Create or synchronize a profile
POST /contacts creates a profile with email, an international phone, or both. At least one is required. POST /contacts/upsert matches by normalized email when supplied; without email it creates a new contact. It never matches or merges by phone. Example body:
{
"email": "ana@example.com",
"phone": "+50688888888",
"firstName": "Ana",
"custom": {
"locale": {"type": "text", "value": "en-US"},
"timezone": {"type": "text", "value": "America/Costa_Rica"}
}
}
Email normalization trims outer whitespace and lowercases; dots and + aliases remain. Phone requires an explicit international country prefix and is stored as E.164; accepted format does not verify ownership or messaging capabilities. New profiles start without an email Marketing subscription. The server assigns identity, source and dates.
Upsert changes only provided fields and custom keys; it requires no revision and returns {contact, created} inside data. Use PATCH /contacts/{id} with the ETag in If-Match for concurrent edit protection. Omitted fields remain; null clears optional fields or custom values. Clearing email or phone must leave the other address. Use PATCH by contact ID to update phone-only profiles; that ID remains stable when addresses change. segmentIds adds assignments, preserves existing ones and also requires segments:write.
GET /contacts accepts email, emailSubscriptionStatus, archived, limit and cursor. Read details with GET /contacts/{id}.
Import in the background
POST /contact-imports accepts up to 1000 rows and 700 KB:
{
"mode": "create",
"rows": [{"contact": {"email": "ana@example.com", "firstName": "Ana"}}]
}
create rejects matches; upsert, the default when mode is omitted, synchronizes. Read progress at GET /contact-imports/{id} and paginated receipts at GET /contact-imports/{id}/rows. There is no external import list. Rows can have different outcomes; profile imports do not import email consent. Rows with only a phone each create a separate contact, including repeated phone numbers. Email, phone, first and last name are principal fields; company, birthday, locale and time zone use their existing custom definitions.
Record explicit consent
With contacts:email:consent:import, record evidence using POST /contacts/{id}/email-consent/evidence: method, email, occurredAt, source, sourceEventId, policyVersion, statement and affirmative: true. Evidence must describe a real action, match the current address and not be future dated.
Use the returned evidence (method, email, occurredAt, evidenceRef) as emailConsent in POST /contacts/{id}/email-subscription with action: "request_subscription", contacts:email:subscribe and If-Match. Recipient confirmation completes the subscription; the link expires after 24 hours.
For established consent, POST /contacts/{id}/email-consent/import accepts {emailConsent} with the explicit import permission. It cannot restore an unsubscribe or clear suppression. Recording evidence alone does not subscribe. Unsubscribe through /email-subscription with {action: "unsubscribe"} and the current revision.
A phone-only contact can participate in segments and automations. An email action records a missing-email omission and continues without consuming email quota. Changing email retains identity and history, resets authorization for the new address, and never redirects an already accepted message.
Retain restrictive history
POST /contacts/{id}/archive and /restore require a revision. Archive retains profile and consent; restoring does not reverse unsubscribes. DELETE /contacts/{id} requires contacts:delete and a revision: it minimizes the profile but retains identity and necessary history to prevent recreation that evades revocation. It is not total physical deletion.