A valid request URL is required to generate request examples{
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "jsmith@example.com",
"avatar_url": "<string>",
"role": "owner",
"role_name": "Owner",
"joined_at": "2023-11-07T05:31:56Z"
}{
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "jsmith@example.com",
"avatar_url": "<string>",
"role": "owner",
"role_name": "Owner",
"joined_at": "2023-11-07T05:31:56Z"
}{
"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"
}
}Put a contact on a team
Adds one of your users to a team, or corrects the role they are on it with — the other half of mirroring your account structure.
Name the contact with contact_id or your own external_id — one of the two. Both is a 400: a request naming two people is one we would have to guess about. The key’s environment scopes both the team and the contact, so a live id asked with a test key answers 404.
Repeating the call is safe and is the point. A membership that already exists with the role you sent changes nothing and publishes nothing — a nightly sync must not put a tenant’s whole roster on the bus every night to say nothing happened. One that exists with a different role is corrected, and customer_member.role_changed is published; a new one publishes customer_member.joined.
It never REMOVES a membership it was not told about. Removing on absence would make an invitation somebody accepted between two runs disappear on the next sweep — use DELETE /v1/contact/customers/members/{contactId} for a removal somebody meant.
Answers 201 for a new membership and 200 for one that already existed.
A valid request URL is required to generate request examples{
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "jsmith@example.com",
"avatar_url": "<string>",
"role": "owner",
"role_name": "Owner",
"joined_at": "2023-11-07T05:31:56Z"
}{
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"email": "jsmith@example.com",
"avatar_url": "<string>",
"role": "owner",
"role_name": "Owner",
"joined_at": "2023-11-07T05:31:56Z"
}{
"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.
255Path Parameters
The team.
Body
The contact's UserKit id. One of this or external_id, never both.
The contact's id in your system. One of this or contact_id, never both.
Defaults to member. owner and member always exist; a tenant may define their own beside them.
"owner"
Response
The membership already existed. The role is whatever it is now.
One person on a team.
The role's key. owner and member always exist; a tenant may define their own beside them.
"owner"
The role's display name, for a screen that should not show a key.
"Owner"