Upsert Contacts
Request
Section titled “Request”Send a JSON array of contacts (not an object). Each contact needs a phone, a
source_ref, or both.
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" } } ]'const res = await fetch("https://api.whatbot.in/public/v1/contacts", { method: "POST", headers: { "X-API-Key": process.env.WHATBOT_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify([ { 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" }, }, ]),});const data = await res.json(); // { upserted: 1, received: 1 }import requests
resp = requests.post( "https://api.whatbot.in/public/v1/contacts", headers={"X-API-Key": "wa_live_xxx"}, json=[ { "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"}, } ], timeout=20,)print(resp.json()) # {'upserted': 1, 'received': 1}Body fields
Section titled “Body fields”| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | One of phone / source_ref | The contact’s phone number, any format. Normalized to E.164 (98765 43210 → +919876543210). Re-sending the same number updates that contact. |
| source_ref | string | One of phone / source_ref | Your 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. |
| name | string | Display name. Also written onto that number’s existing conversations, so the inbox shows your name rather than the WhatsApp profile name. | |
| opt_in | boolean | true marks the contact opted_in. false or omitted leaves them pending — it does not opt them out. | |
| tags | string[] | 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. | |
| attributes | object | Free-form custom fields (email, city, budget, assigned rep…). No pre-declaration needed. Broadcasts can fill a template’s variables from them. | |
| ext_lead_id | string | An external lead reference (e.g. a Salesforce lead id). Stored on the contact and its conversations. Only changed when you send it. |
Response
Section titled “Response”| Field | Type | Required | Description |
|---|---|---|---|
| upserted | number | How many contacts were created or updated. | |
| received | number | How many contacts were in the request body. |
{ "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.
How upsert works
Section titled “How upsert works”- 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,attributesand the opt-in status from the row you send, so leaving one out clears it — and an omittedopt_inresets the contact topending.phone,source_refandext_lead_idare 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, and919876543210all resolve to the same contact.
Add a tag to existing contacts
Section titled “Add a tag to existing contacts”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 -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" }'| Field | Type | Required | Description |
|---|---|---|---|
| phones | string[] | Yes | Phone numbers in any format. Only the first 5,000 are read; duplicates and numbers that can’t be normalized are ignored. |
| tag | string | Yes | The tag to add. Blank returns 422. |
{ "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.
Errors
Section titled “Errors”| Status | Cause |
|---|---|
401 | Missing/invalid API key |
422 | Body wasn’t a JSON array, or a contact had neither phone nor source_ref (/contacts); missing or blank tag (/contacts/tag) |