Skip to content
API PlatformDevelopers
POST/v1/contacts/importbeta

Import contacts in bulk

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 — a full preview with ?dry_run=true
Undo
No bulk undo. Delete created contacts one by one if needed.

Imports up to 1000 rows in one call. New phone numbers are created; existing ones are updated or skipped (on_existing). Invalid rows are skipped and reported.

Run it with ?dry_run=true first: it checks every row and changes nothing.

Try it

Body

The contacts (1–1000).

Group ids.

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

What to do with numbers that already exist.

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/import?dry_run=true' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "rows": [
    {
      "phone": "+15555550123",
      "name": "Jane Doe"
    },
    {
      "phone": "+15555550124",
      "name": "Sam Lee"
    }
  ],
  "tags": [
    "import-sep"
  ],
  "on_existing": "update"
}'

The code reads your key from $API_KEY.

Body

Body fields
FieldTypeWhat it is
rowsrequiredarray of objectThe contacts (1–1000).1–1000 items
phonerequiredstringPhone in international format, e.g. +971501234567.1–40 characters
namestringContact name.0–255 characters
emailstringEmail (needs an "email" custom field).0–255 characters
attributesobjectCustom field values by field key (see GET /v1/custom-fields), e.g. {"city":"Dubai","vip":true}. Values are coerced to the field type; null or "" clears a value.
group_idsarray of string | integerGroup ids.0–20 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”.
on_existingstringWhat to do with numbers that already exist.one of: update, skip · default "update"

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-KeyrequiredheaderRequired here. Any unique string (8–128 characters), e.g. your order id plus the step. Kept 24 hours. Not needed with dry_run=true.pattern ^[A-Za-z0-9._:-]+$ · 8–128 characters

Response 200

Counts and per-row failures. On a dry run: how many rows are valid and every issue.

Response fields
FieldTypeWhat it is
totalrequiredinteger-9007199254740991–9007199254740991
createdrequiredinteger or nullnull in a dry run: known only when the change runs.-9007199254740991–9007199254740991
updatedrequiredinteger or nullnull in a dry run: known only when the change runs.-9007199254740991–9007199254740991
skippedrequiredintegerExisting contacts left untouched (onExisting "skip").-9007199254740991–9007199254740991
invalidrequiredintegerRows rejected by validation before anything was written.-9007199254740991–9007199254740991
failedrequiredinteger-9007199254740991–9007199254740991
failuresrequiredarray of objectInvalid and failed rows (first 50).
rowrequiredinteger-9007199254740991–9007199254740991
coderequiredstring
messagerequiredstring
stopped_earlyrequiredbooleantrue when the plan limit, a permission error, a rate limit or the time limit stopped the import; the rows after it were not written.
group_job_idrequiredstring or null
dry_runbooleantrue when this was a dry run: every check ran and nothing changed.
validintegerDry run: rows that passed every check (they would be created or updated).
notestringDry run: what the preview cannot tell yet.

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. Also returned when the Idempotency-Key header is missing, or was used before with a different body.

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.

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.

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

Import two contacts into a group

Request body

{
  "rows": [
    {
      "phone": "+15555550123",
      "name": "Jane Doe"
    },
    {
      "phone": "+15555550124",
      "name": "Sam Lee"
    }
  ],
  "tags": [
    "import-sep"
  ],
  "on_existing": "update"
}

Response 200

{
  "total": 2,
  "created": 0,
  "updated": 0,
  "skipped": 0,
  "invalid": 0,
  "failed": 0,
  "failures": [],
  "stopped_early": false,
  "dry_run": true,
  "group_job_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_import_contacts, with every field in the JSON body.