Skip to content

Upsert Contacts

Send a JSON array of contacts (not an object). Each contact needs a phone, a source_ref, or both.

Terminal window
curl -X POST https://api.whatbot.in/public/v1/contacts \
-H "X-API-Key: wa_live_xxx" \
-H "Content-Type: application/json" \
-d '[
{
"phone": "9876543210",
"name": "Ravi Kumar",
"opt_in": true,
"tags": ["gold", "vip"],
"source_ref": "crm-lead-4821",
"attributes": {
"email": "ravi@kumarjewels.com",
"city": "Chennai",
"budget": "15L",
"product_interest": "gold coins"
}
}
]'
FieldTypeRequiredDescription
phonestringOne of phone / source_refThe contact’s phone number, any format. Normalized to E.164 (98765 43210 → +919876543210). Re-sending the same number updates that contact.
source_refstringOne of phone / source_refYour own stable reference for the contact (e.g. the CRM record id). Enough on its own to store a contact without a number. Also the key Contact activity looks contacts up by.
namestringDisplay name. Also written onto that number’s existing conversations, so the inbox shows your name rather than the WhatsApp profile name.
opt_inbooleantrue marks the contact opted_in. false or omitted leaves them pending — it does not opt them out.
tagsstring[]Labels for segments and broadcast targeting, e.g. ["gold","vip"]. Replaces the contact’s tags — use /contacts/tag to add one without clearing the rest.
attributesobjectFree-form custom fields (email, city, budget, assigned rep…). No pre-declaration needed. Broadcasts can fill a template’s variables from them.
ext_lead_idstringAn external lead reference (e.g. a Salesforce lead id). Stored on the contact and its conversations. Only changed when you send it.
FieldTypeRequiredDescription
upsertednumberHow many contacts were created or updated.
receivednumberHow many contacts were in the request body.
200
{ "upserted": 1, "received": 1 }

upserted can be lower than received. A row whose phone can’t be normalized and that has no source_ref is skipped, and two rows in one request that share a phone or a source_ref collapse into one (the later row wins). A row whose source_ref names one existing contact while its phone names a different one is refused too — the two would have to be merged by hand. Compare the two numbers rather than assuming every row landed.

  • Keyed by phone or source_ref. A row matching an existing contact on either updates it in place instead of creating a duplicate. Safe to re-run.
  • Send the whole contact each time. An update writes name, tags, attributes and the opt-in status from the row you send, so leaving one out clears it — and an omitted opt_in resets the contact to pending. phone, source_ref and ext_lead_id are the exception: they only change when your row carries a value, so a push without a phone never erases one.
  • Batch freely. Send one contact or thousands in a single array.
  • Phone normalization treats a bare 10-digit number as Indian, so 9876543210, +91 98765 43210, and 919876543210 all resolve to the same contact.

POST https://api.whatbot.in/public/v1/contacts/tag adds one tag to contacts you already have, matched by phone. Unlike POST /contacts, it merges: the contact’s other tags are kept, and a contact that already has the tag is left alone.

cURL
curl -X POST https://api.whatbot.in/public/v1/contacts/tag \
-H "X-API-Key: wa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "phones": ["9876543210", "+91 98123 00002"], "tag": "diwali-2026" }'
FieldTypeRequiredDescription
phonesstring[]YesPhone numbers in any format. Only the first 5,000 are read; duplicates and numbers that can’t be normalized are ignored.
tagstringYesThe tag to add. Blank returns 422.
200
{ "tagged": 2, "received": 2 }

tagged counts contacts that gained the tag. Numbers with no matching contact are not created — upsert them first with POST /contacts.

StatusCause
401Missing/invalid API key
422Body wasn’t a JSON array, or a contact had neither phone nor source_ref (/contacts); missing or blank tag (/contacts/tag)