/v1/contacts/tagstableAdd tags to contacts
- 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
- Remove the tags again with Remove tags from contacts.
Adds tags to up to 500 contacts at once. Tag names that do not exist yet are created. Contacts keep their other tags; tagging twice is harmless.
Try it
Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.
Code and response
curl -X POST 'https://mcp.wa-api.cloud/v1/contacts/tag?dry_run=true' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"contact_ids": [
"48213"
],
"tags": [
"opted-out"
]
}'The code reads your key from $API_KEY.
Body
| Field | Type | What it is |
|---|---|---|
| contact_idsrequired | array of string | integer | Contact ids.1–500 itemsSigned in? Pick one from your data with “My data”. |
| tagsrequired | array of string | integer | Tag names or ids. Names that do not exist yet are created.1–20 itemsSigned in? Pick one from your data with “My data”. |
Headers
| Field | Type | What it is |
|---|---|---|
| Authorizationrequired | header | Bearer $API_KEY — your API key. |
| Api-Version | header | The 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-Key | header | Any 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
Counts and per-contact failures.
| Field | Type | What it is |
|---|---|---|
| requestedrequired | integer | -9007199254740991–9007199254740991 |
| succeededrequired | integer | -9007199254740991–9007199254740991 |
| failedrequired | integer | -9007199254740991–9007199254740991 |
| failuresrequired | array of object | Per-contact failures (first 50). |
| idrequired | string | |
| coderequired | string | |
| messagerequired | string | |
| stopped_earlyrequired | boolean | true when a fatal error (permission, rate limit) stopped the remaining batches; retry those ids. |
| matched | integer or null | By filter: the contacts that matched when the change started (the ones changed).-9007199254740991–9007199254740991 |
| dry_run | boolean | true when this was a dry run: every check ran and nothing changed. |
Errors
Errors are application/problem+json. Branch on code.
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | A field is missing or has the wrong format. |
| 401 | unauthenticated | The Authorization header is missing, the key is unknown, expired or revoked. |
| 403 | entitlement_required | The workspace's plan does not include API access ( |
| 403 | insufficient_scope | The key does not have the permission this operation needs. |
| 409 | conflict | Also returned while a request with the same Idempotency-Key is still running. |
| 429 | rate_limited | The key or workspace went over its rate limit. Wait for |
| 503 | upstream_unavailable | A service behind the API is briefly unavailable. Safe to retry with backoff. |
| 504 | timeout | The change did not finish in time. Retry with the same Idempotency-Key: it never runs twice. |
Examples
Mark a contact as opted out
Request body
{
"contact_ids": [
"48213"
],
"tags": [
"opted-out"
]
}Response 200
{
"requested": 1,
"succeeded": 1,
"failed": 0,
"failures": [],
"stopped_early": false
}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_tag_contacts, with every field in the JSON body.