Create or update contact
Create a contact or update the existing contact matched by phone or email.
Body parameters
| business_id | string |
| phone | string |
| string | |
| first_name | string |
| whatsapp_consent | boolean |
| sms_consent | boolean |
| email_consent | boolean |
| last_name | string |
| tags | string[] |
- A phone is taken in international form (+27821234567) or South African national form (082 123 4567), and is stored and returned as E.164. A number without a leading + is read as South African, so write a foreign number with its +.
- Supplying a phone number or email stores the identifier but does not grant marketing permission. New contacts default to ineligible on every channel.
- Set whatsapp_consent, sms_consent, or email_consent to true only when the recipient explicitly agreed to marketing on that channel. Each assertion requires its matching phone or email in the same request.
- Email consent follows the selected business's confirmation policy. When double opt-in is enabled, channels.email_optin_status is pending until the recipient confirms.
- Pass business_id to assign the contact to a managed business.
- The channel flags sit under channels and follow consent, not the identifiers: read contact.channels.has_whatsapp. A phone alone leaves has_whatsapp and has_sms false until whatsapp_consent: true or sms_consent: true records permission, and changing a phone or email clears them.
- custom_fields values are validated against the workspace's field definitions (Contacts → Fields); unknown keys are rejected. Define the field first, then send it:
"custom_fields": { "plan": "Gold" }. - The created/updated contact is returned flat (fields at the top level), matching GET /api/v1/contacts/{id}.
- The per-channel opted_in_at fields are timestamps for confirmed marketing permission, or null when permission is absent. email_optin_status distinguishes pending confirmation from confirmed permission.
- Creating a contact or adding a tag can enrol it into a workspace-configured journey that later sends a message.
contacts:writeis the trigger boundary; the journey (authored in your workspace, run asynchronously) is the send authority, so this scope does not, and need not, grant per-channel send rights.
curl -X POST "https://app.blueticked.com/api/v1/contacts" \
-H "Authorization: Bearer $BLUETICKED_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: change-me-per-request-0001" \
-d '{
"business_id": "22222222-2222-4222-9222-222222222222",
"phone": "+27800000001",
"email": "sam@example.com",
"first_name": "Sam",
"whatsapp_consent": true,
"sms_consent": true,
"email_consent": true,
"last_name": "Mokoena",
"tags": [
"vip",
"launch"
]
}'Response
{
"id": "ff506e53-e2f0-4d92-884a-dab77d628bad",
"business_id": "22222222-2222-4222-9222-222222222222",
"first_name": "Sam",
"last_name": "Mokoena",
"phone": "+27800000001",
"email": "sam@example.com",
"tags": [
"vip",
"launch"
],
"custom_fields": {
"plan": "Gold",
"next_checkup": "2026-08-01"
},
"channels": {
"has_whatsapp": true,
"whatsapp_opted_in_at": "2026-05-22T10:30:00.000Z",
"has_sms": true,
"sms_opted_in_at": "2026-05-22T10:30:00.000Z",
"has_email": true,
"email_opted_in_at": "2026-05-22T10:30:00.000Z",
"email_optin_status": "confirmed"
},
"suppressed": false,
"created_at": "2026-05-22T10:30:00.000Z",
"updated_at": "2026-05-22T10:30:00.000Z",
"request_id": "req_b2e3d610e10f1b07468721ee98f0e1d2"
}