> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Times de customer

> Um customer é um time: dois papéis, convites que vão para uma caixa de entrada e um customer ativo que viaja em um header.

Um **customer** é a entidade que paga — um workspace, uma empresa ou, em B2C, o
próprio contato. Ele também é um **time**, e esta página é toda forma de entrar
em um.

Customers vivem dentro de um ambiente, como todo registro do plano de clientes.
Dados de teste e dados de produção compartilham o schema e nunca compartilham
linhas — e um assento é uma linha.

## Dois papéis, e nenhum terceiro

`owner` e `member`. Esse é o catálogo inteiro.

| Papel    | Pode                                                                                      |
| -------- | ----------------------------------------------------------------------------------------- |
| `owner`  | Convidar, revogar, reenviar, trocar papéis, remover membros — e tudo o que um member pode |
| `member` | Ler a lista do time e sair                                                                |

Aqui embaixo não existe catálogo de permissões nem forma de definir um papel.
Isso é deliberado: [papéis e permissões](/pt-br/concepts/roles-and-permissions)
existem um plano acima porque a *sua* organização define os dela, e um time B2B
dentro do seu produto quer quem administra e todo o resto. Papéis customizados
chegam quando alguém pedir.

Dois invariantes vêm com os papéis, e os dois espelham o plano de staff:

* **Um customer nunca perde seu último owner.** Rebaixá-lo, removê-lo ou ele
  sair responde `409 last_owner`. Promova outra pessoa antes.
* **Você não pode trocar o seu próprio papel.** É assim que alguém se dá tudo.

## O customer ativo

Um contato pode pertencer a vários customers, então toda chamada diz dentro de
qual ela está agindo — no header **`X-Customer-Id`**.

```bash theme={null}
curl -s $API/v1/contact/customer/members \
  -H "Authorization: Bearer $UK_CONTACT_TOKEN" \
  -H "X-Customer-Id: 8f21…"
```

**Trocar é navegação, não mutação.** Nada é gravado quando alguém muda de time:
você manda um header diferente. Três coisas seguem daí, e são a razão de não ser
um `active_customer_id` guardado na sessão.

<ResponseField name="O header identifica; o vínculo autoriza">
  Um id de customer que alguém cole não casa com nenhuma linha de vínculo e
  responde `404` — o mesmo `404` de um customer que não existe. Revogar um
  vínculo vale na *próxima requisição*, não no próximo login.
</ResponseField>

<ResponseField name="Duas abas continuam corretas">
  Não existe um cursor compartilhado para uma aba mover debaixo da outra.
</ResponseField>

<ResponseField name="O JWT não sobrevive à escolha">
  As claims seguem o que *esta chamada* pediu, e o próximo mint está a no máximo
  cinco minutos de distância.
</ResponseField>

Sem o header, o **vínculo mais antigo** do contato responde — a mesma queda que
uma sessão de staff faz para a organização padrão. Um contato sem time nenhum
não é um erro: é todo contato B2C.

Leia a lista em `GET /v1/contact/customers`:

```json theme={null}
{
  "customers": [
    { "id": "8f21…", "name": "Contoso", "role": "owner",  "joined_at": "…" },
    { "id": "b7c4…", "name": "Initech", "role": "member", "joined_at": "…" }
  ]
}
```

## O time dentro do JWT

`POST /v1/contact/token` coloca o customer ativo no
[JWT de sessão](/pt-br/customer-auth/session-tokens), para o seu backend
responder "qual time, e essa pessoa pode administrá-lo" **offline**:

```json theme={null}
{
  "sub": "3f9a…",
  "sid": "c1d2…",
  "verified": true,
  "customer_id": "8f21…",
  "customer_role": "owner",
  "exp": 1735689600
}
```

Mande `X-Customer-Id` no mint e as claims seguem. Nomear um customer ao qual o
contato não pertence é recusado com `404` em vez de simplesmente omitir a claim
— um token sem o campo que o seu backend usa como porta é pior que um erro.

As duas claims ficam ausentes quando o contato não pertence a customer nenhum.
Trate ausente como "sem time", nunca como "sem necessidade de checar".

## Abrindo um time

```bash theme={null}
curl -s $API/v1/contact/customers \
  -H "Authorization: Bearer $UK_CONTACT_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Contoso" }'
```

O contato que chamou vira o owner, para que a guarda do último owner tenha o que
defender antes de qualquer outra pessoa chegar. Um visitante anônimo é recusado
(`identity_required`).

O seu próprio servidor também pode criar customers direto, que é o formato usual
no [modo federado](/pt-br/customer-auth/federated) — lá o seu produto já sabe
quem trabalha com quem.

## Convites

```bash theme={null}
curl -s $API/v1/contact/customer/invitations \
  -H "Authorization: Bearer $UK_CONTACT_TOKEN" \
  -H "X-Customer-Id: 8f21…" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "bob@example.com", "role": "member" }'
```

Só owner. Vale por **7 dias**, um convite pendente por endereço por customer, e
o e-mail sai em nome da sua organização — quem recebe é cliente do *seu* produto
e pode nunca ter ouvido falar do UserKit.

<Note>
  A resposta não diz nada sobre o endereço já ter conta com você. O único conflito
  que ela reporta — `already_member` — é participação no time de quem chamou, que
  essa pessoa já lê na lista do time. Todo outro caso segue o caminho idêntico e
  produz a resposta idêntica.
</Note>

**Reenviar gera um token novo.**
`POST /v1/contact/customer/invitations/{id}/resend` rotaciona o hash e a
validade, então o link da mensagem anterior para de funcionar: um convite é uma
credencial, e nunca pode haver duas vivas para o mesmo assento. **Revogar**
(`DELETE …/{id}`) fecha o assento de vez.

### Aceitar exige duas provas

`POST /v1/contact/invitations/accept` precisa de um contato autenticado **e** do
token.

O token prova o convite — que este assento foi oferecido a este endereço. A
sessão prova a pessoa. Nenhum dos dois basta sozinho: um link encaminhado
sentaria quem o abrisse.

Por isso **o endereço convidado precisa ser uma identidade do contato que
aceita**, ou a chamada responde `403 invitation_not_yours`. É a mesma regra que
todo fluxo por endereço deste plano já segue: um e-mail é um *atributo* até algo
transformá-lo em [aresta de identidade](/pt-br/concepts/contacts#arestas-de-identidade),
e um atributo é algo que qualquer um digita.

Um endereço vira aresta pelo cadastro hospedado, por um magic link, por um
código de seis dígitos, por um login social em que o provedor atestou o
endereço, ou pelo seu próprio servidor chamando `POST /v1/contacts` com ele.
Passar um endereço para `/v1/boot` no modo federado deliberadamente não faz
isso.

<Warning>
  Um convite pertence a **um ambiente**. Um contato do outro é recusado com o
  mesmo `invalid_token` de um link expirado — usado, expirado, revogado, nunca
  existiu e ambiente errado são uma resposta só, de propósito.
</Warning>

## Eventos

Toda mutação publica um fato que você consome pelos
[eventos](/pt-br/guides/events):

| Evento                         | Quando                                                 |
| ------------------------------ | ------------------------------------------------------ |
| `customer.created`             | Um time foi aberto                                     |
| `customer_member.invited`      | Um assento foi oferecido                               |
| `customer_member.joined`       | Um convite foi aceito                                  |
| `customer_member.role_changed` | Um papel mudou                                         |
| `customer_member.removed`      | Alguém foi removido, ou saiu (`left: true` no payload) |

Payloads carregam ids, nunca endereços — o e-mail fica fora de toda fila e todo
log por onde o envelope passa.
