Contacts
People you talk to. A contact has one or more channel identities (WhatsApp number, Instagram handle, email…).
/contactsAvailablePOST/contactsBetaPOST/contacts/importAvailableGET/contacts/{contact_id}AvailablePATCH/contacts/{contact_id}AvailableDELETE/contacts/{contact_id}Coming soonPOST/contacts/{contact_id}/mergeComing soonGET/custom-fieldsComing soonPOST/custom-fieldsComing soonGET/tagsComing soonGET/stagesAvailableList contacts
Section titled “List contacts”/v1/contactsAvailableNewest activity first. q matches name, phone and email with Arabic-aware folding (أحمد = احمد = إحمد).
Query parameters
| Field | Type | Description |
|---|---|---|
q |
string | Free-text search. |
tag |
string | Only contacts with this tag. |
stage_id |
string | Only contacts in this lifecycle stage. |
channel_type |
“whatsapp” | “instagram” | “messenger” | “sms” | “email” | “webchat” | |
limit |
integer | |
cursor |
string | The next_cursor from the previous page. |
curl "https://api.waasl.io/v1/contacts" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/contacts", { headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.get( "https://api.waasl.io/v1/contacts", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()print(r.json())Response 200 — A page of contacts
{ "data": [ { "id": "ct_8Jk2Lm", "name": "Sara Al-Mutairi", "identities": [ { "channel_type": null, "identifier": null, "display_name": null } ], "email": "string", "language": "ar", "dialect": "kuwaiti", "vip": true, "blocked": true, "tags": [ "vip" ], "stage_id": "customer", "consents": {}, "custom_fields": {}, "last_activity_at": "2026-10-01T08:15:00Z" } ], "next_cursor": "MjAyNi0xMC0wMVQwODoxNTowMC4wMDBafGN2XzNQekE5MQ", "has_more": true}Errors: 401 Missing, invalid or revoked API key
Create or update a contact
Section titled “Create or update a contact”/v1/contactsBetaUpserts by identity: if a contact already owns one of the given identities it is
updated, otherwise a new contact is created. Phone numbers are normalised to E.164.
Returns 201 when created and 200 when an existing contact was updated.
Headers
| Field | Type | Description |
|---|---|---|
Idempotency-Key |
string | A unique key (UUID recommended). Retrying with the same key returns the original result instead of acting twice. Kept for 24 hours. |
Request body
| Field | Type | Description |
|---|---|---|
name |
string | |
identities required |
object[] | |
identities[].channel_type |
“whatsapp” | “instagram” | “messenger” | “sms” | “email” | “webchat” | Only whatsapp is live; the others are coming soon. |
identities[].identifier |
string | Phone in E.164 (or a local 8-digit Kuwaiti number), handle or email. |
identities[].display_name |
string | |
email |
string (email) | |
language |
string | |
vip |
boolean | |
tags |
string[] | |
stage_id |
string | |
custom_fields |
object | Keys must exist in /custom-fields (coming soon). |
consents |
object | Marketing opt-in per channel — required before sending marketing templates. |
curl -X POST "https://api.waasl.io/v1/contacts" \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234" } ], "email": "sara@example.com", "language": "ar", "tags": [ "vip", "ramadan-2026" ], "custom_fields": { "plan": "Keto 30", "city": "Salmiya" }}'const res = await fetch("https://api.waasl.io/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234" } ], "email": "sara@example.com", "language": "ar", "tags": [ "vip", "ramadan-2026" ], "custom_fields": { "plan": "Keto 30", "city": "Salmiya" } }),});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import os, uuidimport requests
r = requests.post( "https://api.waasl.io/v1/contacts", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())}, json={ "name": "Sara Al-Mutairi", "identities": [{ "channel_type": "whatsapp", "identifier": "+96550001234" }], "email": "sara@example.com", "language": "ar", "tags": ["vip", "ramadan-2026"], "custom_fields": { "plan": "Keto 30", "city": "Salmiya" } },)r.raise_for_status()print(r.json())Response 200 — Existing contact updated
{ "id": "ct_8Jk2Lm", "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234", "display_name": "string" } ], "email": "string", "language": "ar", "dialect": "kuwaiti", "vip": true, "blocked": true, "tags": [ "vip" ], "stage_id": "customer", "consents": {}, "custom_fields": {}, "last_activity_at": "2026-10-01T08:15:00Z"}Errors: 422 The request body is invalid
Import contacts in bulk
Section titled “Import contacts in bulk”/v1/contacts/importAvailableUpserts up to 1,000 contacts per call. Rows that fail validation are reported individually; the rest are imported.
Request body
| Field | Type | Description |
|---|---|---|
contacts required |
object[] | |
contacts[].name |
string | |
contacts[].identities |
object[] | |
contacts[].email |
string (email) | |
contacts[].language |
string | |
contacts[].vip |
boolean | |
contacts[].tags |
string[] | |
contacts[].stage_id |
string | |
contacts[].custom_fields |
object | Keys must exist in /custom-fields (coming soon). |
contacts[].consents |
object | Marketing opt-in per channel — required before sending marketing templates. |
curl -X POST "https://api.waasl.io/v1/contacts/import" \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "name": "Sara", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234" } ] }, { "name": "Omar", "identities": [ { "channel_type": "whatsapp", "identifier": "55512345" } ], "tags": [ "lead" ] } ]}'const res = await fetch("https://api.waasl.io/v1/contacts/import", { method: "POST", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "contacts": [ { "name": "Sara", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234" } ] }, { "name": "Omar", "identities": [ { "channel_type": "whatsapp", "identifier": "55512345" } ], "tags": [ "lead" ] } ] }),});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.post( "https://api.waasl.io/v1/contacts/import", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"}, json={ "contacts": [{ "name": "Sara", "identities": [{ "channel_type": "whatsapp", "identifier": "+96550001234" }] }, { "name": "Omar", "identities": [{ "channel_type": "whatsapp", "identifier": "55512345" }], "tags": ["lead"] }] },)r.raise_for_status()print(r.json())Response 200 — Import result
{ "created": 1, "updated": 1, "failed": [ { "index": 0, "code": "invalid_phone" } ]}Retrieve a contact
Section titled “Retrieve a contact”/v1/contacts/{contact_id}AvailablePath parameters
| Field | Type | Description |
|---|---|---|
contact_id required |
string |
curl "https://api.waasl.io/v1/contacts/ct_8Jk2Lm" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/contacts/ct_8Jk2Lm", { headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.get( "https://api.waasl.io/v1/contacts/ct_8Jk2Lm", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()print(r.json())Response 200 — The contact
{ "id": "ct_8Jk2Lm", "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234", "display_name": "string" } ], "email": "string", "language": "ar", "dialect": "kuwaiti", "vip": true, "blocked": true, "tags": [ "vip" ], "stage_id": "customer", "consents": {}, "custom_fields": {}, "last_activity_at": "2026-10-01T08:15:00Z"}Errors: 404 Not found in this workspace
Update a contact
Section titled “Update a contact”/v1/contacts/{contact_id}AvailableOnly the fields you send change. Send tags to replace the tag set; set blocked to stop all outbound messages to this contact.
Path parameters
| Field | Type | Description |
|---|---|---|
contact_id required |
string |
Request body
| Field | Type | Description |
|---|---|---|
name |
string | |
email |
string (email) | |
language |
string | |
vip |
boolean | |
blocked |
boolean | |
tags |
string[] | |
stage_id |
string | null | |
custom_fields |
object |
curl -X PATCH "https://api.waasl.io/v1/contacts/ct_8Jk2Lm" \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "vip": true, "tags": [ "vip", "renewed" ], "stage_id": "customer"}'const res = await fetch("https://api.waasl.io/v1/contacts/ct_8Jk2Lm", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "vip": true, "tags": [ "vip", "renewed" ], "stage_id": "customer" }),});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.patch( "https://api.waasl.io/v1/contacts/ct_8Jk2Lm", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"}, json={ "vip": True, "tags": ["vip", "renewed"], "stage_id": "customer" },)r.raise_for_status()print(r.json())Response 200 — Updated contact
{ "id": "ct_8Jk2Lm", "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234", "display_name": "string" } ], "email": "string", "language": "ar", "dialect": "kuwaiti", "vip": true, "blocked": true, "tags": [ "vip" ], "stage_id": "customer", "consents": {}, "custom_fields": {}, "last_activity_at": "2026-10-01T08:15:00Z"}Errors: 404 Not found in this workspace · 422 The request body is invalid
Delete a contact
Section titled “Delete a contact”/v1/contacts/{contact_id}Coming soonPermanently erases the contact, its identities and its conversation history (GDPR / PDPL right to erasure).
Path parameters
| Field | Type | Description |
|---|---|---|
contact_id required |
string |
curl -X DELETE "https://api.waasl.io/v1/contacts/ct_8Jk2Lm" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/contacts/ct_8Jk2Lm", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);import osimport requests
r = requests.delete( "https://api.waasl.io/v1/contacts/ct_8Jk2Lm", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()Returns 204 — Deleted.
Errors: 404 Not found in this workspace
Merge contacts
Section titled “Merge contacts”/v1/contacts/{contact_id}/mergeComing soonFolds source_contact_id into this contact — identities, tags and conversations move over; the source is deleted.
Path parameters
| Field | Type | Description |
|---|---|---|
contact_id required |
string |
Request body
| Field | Type | Description |
|---|---|---|
source_contact_id required |
string |
curl -X POST "https://api.waasl.io/v1/contacts/ct_8Jk2Lm/merge" \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_contact_id": "ct_8Jk2Lm"}'const res = await fetch("https://api.waasl.io/v1/contacts/ct_8Jk2Lm/merge", { method: "POST", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "source_contact_id": "ct_8Jk2Lm" }),});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.post( "https://api.waasl.io/v1/contacts/ct_8Jk2Lm/merge", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"}, json={ "source_contact_id": "ct_8Jk2Lm" },)r.raise_for_status()print(r.json())Response 200 — The surviving contact
{ "id": "ct_8Jk2Lm", "name": "Sara Al-Mutairi", "identities": [ { "channel_type": "whatsapp", "identifier": "+96550001234", "display_name": "string" } ], "email": "string", "language": "ar", "dialect": "kuwaiti", "vip": true, "blocked": true, "tags": [ "vip" ], "stage_id": "customer", "consents": {}, "custom_fields": {}, "last_activity_at": "2026-10-01T08:15:00Z"}List custom fields
Section titled “List custom fields”/v1/custom-fieldsComing sooncurl "https://api.waasl.io/v1/custom-fields" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/custom-fields", { headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.get( "https://api.waasl.io/v1/custom-fields", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()print(r.json())Response 200 — Field definitions
{ "data": [ { "key": "string", "label": "string", "type": "text", "options": [ "string" ] } ]}Create a custom field
Section titled “Create a custom field”/v1/custom-fieldsComing soonRequest body
| Field | Type | Description |
|---|---|---|
key required |
string | |
label |
string | |
type required |
“text” | “number” | “date” | “boolean” | “select” | |
options |
string[] |
curl -X POST "https://api.waasl.io/v1/custom-fields" \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "plan", "label": "Subscription plan", "type": "text"}'const res = await fetch("https://api.waasl.io/v1/custom-fields", { method: "POST", headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "key": "plan", "label": "Subscription plan", "type": "text" }),});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.post( "https://api.waasl.io/v1/custom-fields", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"}, json={ "key": "plan", "label": "Subscription plan", "type": "text" },)r.raise_for_status()print(r.json())Response 201 — Created
{ "key": "string", "label": "string", "type": "text", "options": [ "string" ]}List tags
Section titled “List tags”/v1/tagsComing soonEvery tag used in the workspace, with how many contacts carry it.
curl "https://api.waasl.io/v1/tags" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/tags", { headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.get( "https://api.waasl.io/v1/tags", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()print(r.json())Response 200 — Tags
{ "data": [ { "name": "vip", "contacts": 42 } ]}List lifecycle stages
Section titled “List lifecycle stages”/v1/stagesAvailableThe pipeline stages a contact or conversation can be in (New lead → Hot lead → Customer…).
curl "https://api.waasl.io/v1/stages" \ -H "Authorization: Bearer $WAASL_API_KEY"const res = await fetch("https://api.waasl.io/v1/stages", { headers: { Authorization: `Bearer ${process.env.WAASL_API_KEY}` },});if (!res.ok) throw new Error((await res.json()).code);const data = await res.json();import osimport requests
r = requests.get( "https://api.waasl.io/v1/stages", headers={"Authorization": f"Bearer {os.environ['WAASL_API_KEY']}"},)r.raise_for_status()print(r.json())Response 200 — Stages, in order
[ { "id": "hot_lead", "name": "عميل مهتم", "emoji": "🔥", "order": 0 }]