A valid request URL is required to generate request examples{
"contact": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "<string>",
"avatar_url": "<string>",
"locale": "en",
"identified": true,
"email_verified": true,
"attribution": {
"utm_source": "<string>",
"utm_medium": "<string>",
"utm_campaign": "<string>",
"utm_term": "<string>",
"utm_content": "<string>",
"referrer": "<string>",
"landing_page": "<string>"
},
"first_seen_at": "2023-11-07T05:31:56Z",
"last_seen_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z"
},
"created": true
}{
"contact": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "<string>",
"avatar_url": "<string>",
"locale": "en",
"identified": true,
"email_verified": true,
"attribution": {
"utm_source": "<string>",
"utm_medium": "<string>",
"utm_campaign": "<string>",
"utm_term": "<string>",
"utm_content": "<string>",
"referrer": "<string>",
"landing_page": "<string>"
},
"first_seen_at": "2023-11-07T05:31:56Z",
"last_seen_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z"
},
"created": true
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Identify a contact
Server-side identity: create or update a contact from whatever identities the developer’s backend knows, and the team they belong to. At least one of external_id, email or anonymous_id is required, and customer always is.
customer is required, which makes every login the sync: the team is upserted by your own id for it, the membership is created or corrected, and nothing is announced on a call that changed nothing. Without it a B2B tenant’s roster stayed empty forever and every question the customer plane can ask about a team had nothing to resolve against — see the field for the full argument.
Resolution order is external_id, then email — external_id is the developer’s own primary key and the stronger claim. When an anonymous_id arrives alongside an identifying kind, the visitor it belonged to is folded in, attribution and all. Everything runs in one transaction: an identify that half-merged a visitor would be worse than one that failed.
Answers 201 when the contact was created, 200 when it already existed. Send an Idempotency-Key if this call is retried by a client, a queue or a deploy: the retry then gets that same 201 back rather than the 200 the second call would otherwise produce.
A valid request URL is required to generate request examples{
"contact": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "<string>",
"avatar_url": "<string>",
"locale": "en",
"identified": true,
"email_verified": true,
"attribution": {
"utm_source": "<string>",
"utm_medium": "<string>",
"utm_campaign": "<string>",
"utm_term": "<string>",
"utm_content": "<string>",
"referrer": "<string>",
"landing_page": "<string>"
},
"first_seen_at": "2023-11-07T05:31:56Z",
"last_seen_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z"
},
"created": true
}{
"contact": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "<string>",
"avatar_url": "<string>",
"locale": "en",
"identified": true,
"email_verified": true,
"attribution": {
"utm_source": "<string>",
"utm_medium": "<string>",
"utm_campaign": "<string>",
"utm_term": "<string>",
"utm_content": "<string>",
"referrer": "<string>",
"landing_page": "<string>"
},
"first_seen_at": "2023-11-07T05:31:56Z",
"last_seen_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z"
},
"created": true
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Authorizations
An organization API key, uk_sk_live_… or uk_sk_test_…. The environment is resolved from the stored key row on every request, never from a request parameter.
Every route behind this credential is rate limited per key — 1000 requests a minute — with a second ceiling of 3000 a minute across all the keys of one environment, so splitting your traffic across keys isolates it and minting more keys does not buy more of it. A test key can never spend a live key's allowance. Every response carries the current state in headers; see the rate-limits guide.
Headers
A key you choose — a UUID, or your own identifier for the operation — that makes this write safe to retry. Send the same key again and you get the same response back: the same status, the same body, and Idempotent-Replay: true. The handler does not run a second time.
The key is scoped to your API key and the route, so keys never collide between environments or tenants. It is remembered for 24 hours. Reusing it for a different request is refused (422), and a second request arriving while the first is still running is refused too (409) — retry that one in a moment.
A request that failed with a 5xx or was rate limited leaves the key spendable: retry it with the same key.
255Body
The developer's own primary key for this person.
Where a WhatsApp message can reach this person, in the international form with the country code (5511987654321). Punctuation is fine; the number is normalised to E.164 without the plus before it is stored.
It becomes an identity of the contact, beside the email and your own external_id, so the merge and the erasure already know about it. It never resolves a contact, though: operators recycle numbers, and whoever gets a reused one is not the person who had it. A number is attached when it is free and ignored when it already belongs to somebody — a message that does not go out, rather than one that goes to the wrong stranger.
A number with no country code, or one carrying a national trunk prefix (011…), is a 400. We do not guess the country: a default would make the same digits mean different people in different environments.
The device id captured before this person was known.
Required. The team this person belongs to, named by YOUR id for it.
Upserted on every call, so a login is the whole sync: the team is created the first time and found every time after, the membership is created or its role corrected, and a call that changed nothing announces nothing. Two people of the same account land on the same team.
It is required rather than optional on purpose. Everything the customer plane can answer about a team — entitlements, a customer-scoped onboarding step, seat billing — needs one to resolve against, and you are the only system that knows which one this person belongs to. Optional, it would be the field everybody means to send later: the roster stays empty, every team question keeps answering "no team", and nothing says why.
Show child attributes
Show child attributes
First touch, captured on the visitor at first load and preserved through a merge. All fields optional; empty means unknown.
Show child attributes
Show child attributes