Skip to content
API PlatformDevelopers
POST/v1/contactsstable

Create or update a contact

Needs
Create and edit contacts, tags, contact groups and custom fields (crm:write)
Plan
Any plan with API access
Limits
60 a minute per key
Dry run
Yes — every check runs with ?dry_run=true, nothing changes
Undo
Update the contact again, or delete it if it was just created.

Creates a contact, or updates the one that already has this phone number — so calling it twice is safe. Groups and tags you pass are added; existing ones are kept.

This is the call to use when syncing from a shop or CRM.

Try it

Body

International format with country code, e.g. `+15555550123`. Local numbers starting with 0 are refused.

Full name.

Email address (stored in the `email` custom field).

Custom field values by key. `null` or `""` clears a value. Unknown keys are refused with the list of valid ones.

Group ids.

Tag names or ids. Names that do not exist yet are created.

Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.

Test mode (dry run): nothing will change
Turns test mode off for this page only. It switches back when you leave the page.

Code and response

curl -X POST 'https://mcp.wa-api.cloud/v1/contacts?dry_run=true' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "phone": "+15555550123",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "tags": [
    "shopify",
    "vip"
  ],
  "attributes": {
    "order_count": 4
  }
}'

The code reads your key from $API_KEY.

Body

Body fields
FieldTypeWhat it is
phonerequiredstringInternational format with country code, e.g. +15555550123. Local numbers starting with 0 are refused.3–32 characters
namestringFull name.1–255 characters
emailstringEmail address (stored in the email custom field).0–255 characters
attributesobjectCustom field values by key. null or "" clears a value. Unknown keys are refused with the list of valid ones.
group_idsarray of string | integerGroup ids.0–50 items
tagsarray of string | integerTag names or ids. Names that do not exist yet are created.0–20 itemsSigned in? Pick one from your data with “My data”.

Headers

Headers
FieldTypeWhat it is
AuthorizationrequiredheaderBearer $API_KEY — your API key.
Api-VersionheaderThe API version to use, e.g. 2026-10-01. Default: the version your key is pinned to.one of: 2026-10-01 · pattern ^\d{4}-\d{2}-\d{2}$
Idempotency-KeyheaderAny unique string (8–128 characters). A retry with the same key returns the first answer instead of running twice. Kept 24 hours.pattern ^[A-Za-z0-9._:-]+$ · 8–128 characters

Response 200

The contact, and whether it was created or updated.

Response fields
FieldTypeWhat it is
idrequiredstring
actionrequiredstringone of: created, updated, unchanged
contactrequiredobject
idrequiredstring
namerequiredstring or null
phonerequiredstring or nullAs the platform shows it to this staff member: E.164 digits, or masked (e.g. "*******12345") when the workspace hides numbers from agents. Never unmasked here.
phone_maskedrequiredboolean
whatsapp_user_idrequiredstring or null
dndrequiredbooleanDo-not-disturb: the contact receives no broadcasts/marketing.
starredrequiredboolean or nullnull only when it could not be read this time (unavailable includes profile_extras).
created_atrequiredstring or null
updated_atrequiredstring or null
emailstring or null
web_visitor_idstring or null
tagsarray of object
idrequiredstring
namerequiredstring or null
groupsarray of object
idrequiredstring
namerequiredstring or null
attributesarray of objectEvery custom field value this staff member may see (manager-only fields are hidden from agents; deleted fields never appear), at most 100. Text values longer than 2000 characters are clipped.
keyrequiredstringThe custom field key (as in GET /v1/custom-fields).
labelrequiredstring or nullThe field label shown in the panel.
typerequiredstring or nulltext, textarea, number, int, float, decimal, email, phone, url, select, radio, multiselect, checkbox, boolean, date or datetime.
valuerequiredanyThe value as the panel shows it: text; checkbox/boolean "Yes"/"No"; date "YYYY-MM-DD"; datetime "YYYY-MM-DD HH:MM:SS" (UTC); multiselect "a, b"; int/float numbers; decimal a string with 8 decimals.
displayrequiredstring or nullvalue as text.
typed_valuerequiredstring | number | boolean | array of string or nullvalue typed by the field type: number/int/float/decimal → number; checkbox/boolean → true/false; date → "YYYY-MM-DD"; datetime → ISO-8601 UTC; multiselect → array of the chosen values; select/radio/text/… → string. null when empty or unreadable.
conversationsarray of objectNewest first (at most 10), a timeline summary; read messages with the inbox tools.
idrequiredstring
channel_idrequiredstring or null
staterequiredstring or null
assignedrequiredboolean
assigned_staff_idrequiredstring or null
created_atrequiredstring or null
last_activity_atrequiredstring or null
last_activity_atstring or nullLatest conversation activity (from the conversation list).
unavailablearray of stringPresent only when part of the contact could not be read this time (retry later for it): profile_extras = starred, createdAt, updatedAt and webVisitorId are null because they are unknown, not empty; conversations = the timeline (and lastActivityAt) is empty because it could not be read, not because there are none.
dry_runbooleantrue when this was a dry run: every check ran and nothing changed.

Errors

Errors are application/problem+json. Branch on code.

StatusCodeWhen
400invalid_input

A field is missing or has the wrong format. errors[] points at each field. Too many other contacts' numbers contain this one to find the existing contact exactly (reason: lookup_incomplete): nothing is created, so no duplicate; update the contact by id instead.

401unauthenticated

The Authorization header is missing, the key is unknown, expired or revoked.

403entitlement_required

The workspace's plan does not include API access (api_access).

403forbidden

Your plan contact limit is reached (reason: plan_contact_limit).

403insufficient_scope

The key does not have the permission this operation needs.

409conflict

Also returned while a request with the same Idempotency-Key is still running.

429rate_limited

The key or workspace went over its rate limit. Wait for Retry-After seconds.

502upstream_error

The contact service answered unexpectedly. Retry with the same Idempotency-Key.

503upstream_unavailable

A service behind the API is briefly unavailable. Safe to retry with backoff.

504timeout

The change did not finish in time. Retry with the same Idempotency-Key: it never runs twice.

Examples

A new customer from your shop

Request body

{
  "phone": "+15555550123",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "tags": [
    "shopify",
    "vip"
  ],
  "attributes": {
    "order_count": 4
  }
}

Response 200

{
  "id": "48213",
  "action": "created",
  "contact": {
    "id": "48213",
    "name": "Jane Doe",
    "phone": "+15555550123",
    "phone_masked": false,
    "email": "jane@example.com",
    "dnd": false,
    "starred": false,
    "tags": [
      {
        "id": "17",
        "name": "vip"
      }
    ],
    "groups": [
      {
        "id": "5",
        "name": "Newsletter"
      }
    ],
    "created_at": "2026-09-20T08:14:03Z",
    "updated_at": "2026-09-23T16:40:11Z",
    "whatsapp_user_id": null
  }
}

Webhook events

These events can fire after this call. Subscribe an endpoint to hear about them.

Operation path

The same operation is also at POST /v1/ops/crm_upsert_contact, with every field in the JSON body.