> ## 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.

# Autenticação hosted

> O UserKit é dono da conta: cadastro, login, verificação e recuperação, nenhum deles enumerável.

No modo hosted o UserKit guarda a senha e é dono da conta. Os formulários são seus;
estes endpoints são as regras por trás deles.

Todo endpoint aqui é superfície pública alcançada com uma **chave publicável** no
corpo da requisição, a partir do seu próprio domínio. A postura é a mais rígida da
API: respostas não enumeráveis, caminhos de senha em tempo constante, rate limits
estritos e tokens de uso único para tudo que chega por e-mail.

<Note>
  Estes endpoints exigem que o ambiente esteja em modo `hosted`. Em um ambiente
  federado, eles respondem `409 environment_not_hosted`.
</Note>

## Cadastro

```bash theme={null}
curl -s $API/v1/contact-auth/signup \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "email": "grace@example.com",
    "password": "Compiler1",
    "name": "Grace Hopper",
    "anonymous_id": "anon_2f9c1b"
  }'
```

```json 202 — sempre theme={null}
{ "message": "confira seu e-mail para confirmar o endereço" }
```

Sempre `202`. A resposta nunca revela se o endereço já tem conta. Três coisas
diferentes acontecem por trás dessa única resposta:

<Steps>
  <Step title="Um endereço novo">
    O contato é criado com a senha anexada — não há nada aqui para tomar — e um
    e-mail de verificação sai. `email_verified` continua falso até o link ser
    clicado.
  </Step>

  <Step title="Um endereço que já tem senha">
    Nada é criado. Um aviso de "você já tem uma conta" vai para a **caixa de
    entrada**, nunca para quem chamou.
  </Step>

  <Step title="Um contato que o seu servidor criou, ainda sem senha">
    A senha escolhida espera **no token de verificação** e só se anexa quando o
    link comprovar o e-mail.
  </Step>
</Steps>

<Note>
  Enquanto o ambiente está em [modo lista de espera](/pt-br/customer-auth/waitlist),
  esta mesma chamada coloca a pessoa na fila em vez de criar conta, e responde
  `{ "waitlist": true }`. A senha escolhida fica guardada na entrada até ela ser
  liberada — o terceiro caso abaixo, uma camada acima.
</Note>

<Warning>
  Esse terceiro caso é o que vale entender. Se a senha se anexasse na hora, qualquer
  um poderia se cadastrar com o endereço de um contato que você pré-criou e capturar
  a conta. Fazer a senha esperar no token fecha isso estruturalmente, e não com uma
  verificação que alguém pode esquecer.
</Warning>

Passar `anonymous_id` absorve o visitante que essa pessoa já era no novo contato,
atribuição incluída.

Rate limit de 10 por hora por IP.

## Verificar o e-mail

O link do e-mail carrega um token `uk_cv_…`, válido por 24 horas. Sua página lê da
query string e faz o POST:

```bash theme={null}
curl -s $API/v1/contact-auth/verify-email \
  -H 'Content-Type: application/json' \
  -d '{ "token": "uk_cv_…" }'
```

```json 200 theme={null}
{
  "contact": { "id": "…", "identified": true, "email_verified": true },
  "token": "uk_ct_…",
  "expires_at": "2026-08-28T12:00:00Z",
  "verified": true
}
```

Três coisas acontecem de uma vez: o e-mail é comprovado, qualquer senha esperando no
token se anexa, e uma **sessão verificada** volta — a pessoa clicou a partir da
caixa de entrada, não faça ela logar de novo.

### Reenviar

```bash theme={null}
curl -s $API/v1/contact-auth/resend-verification \
  -H 'Content-Type: application/json' \
  -d '{ "publishable_key": "uk_pk_live_…", "email": "grace@example.com" }'
```

`202` sempre. Nada sai para endereços desconhecidos ou já verificados. Rate limit de
5 por hora por IP — cada requisição envia e-mail para a caixa de alguém.

## Login

```bash theme={null}
curl -s $API/v1/contact-auth/login \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "email": "grace@example.com",
    "password": "Compiler1"
  }'
```

```json 200 theme={null}
{
  "contact": { "id": "…", "name": "Grace Hopper" },
  "token": "uk_ct_…",
  "expires_at": "2026-08-28T12:00:00Z",
  "verified": true
}
```

E-mail desconhecido, contato sem senha e senha errada produzem o mesmo
`401 invalid_credentials` — e levam aproximadamente o mesmo tempo, porque o caminho
sem senha roda uma comparação bcrypt descartável. Sem isso, só contas reais pagariam
as dezenas de milissegundos, e a latência sozinha responderia "esse endereço
existe?".

Provar a senha prova a conta, então a sessão é verificada.

Rate limit de 20 por 5 minutos por IP.

## Recuperar a senha

<Steps>
  <Step title="Pedir">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/forgot-password \
      -H 'Content-Type: application/json' \
      -d '{ "publishable_key": "uk_pk_live_…", "email": "grace@example.com" }'
    ```

    `202` incondicional. Rate limit de 5 por hora por IP.
  </Step>

  <Step title="Redefinir">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/reset-password \
      -H 'Content-Type: application/json' \
      -d '{ "token": "uk_cr_…", "password": "Nanoseconds1" }'
    ```

    `204`. Rate limit de 10 por hora por IP.
  </Step>
</Steps>

A redefinição faz três coisas além de trocar a senha:

* **todas** as sessões daquele contato são revogadas, na hora — uma redefinição
  normalmente significa que outra pessoa pode estar com a credencial;
* o e-mail conta como comprovado, porque o link chegou nele;
* um contato que nunca havia sido identificado passa a ser.

<Note>
  Este fluxo também funciona para contatos que nunca tiveram senha — os que o seu
  servidor criou com `POST /v1/contacts`. Receber o link comprova o e-mail, então
  "redefinir" faz as vezes de "definir uma senha".
</Note>

## Magic links

Login sem senha, e funciona nos **dois** modos. No modo federado, é também a única
coisa capaz de promover um e-mail de atributo a identidade comprovada.

<Steps>
  <Step title="Pedir">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/magic-link \
      -H 'Content-Type: application/json' \
      -d '{ "publishable_key": "uk_pk_live_…", "email": "grace@example.com" }'
    ```

    `202` incondicional. Válido por uma hora. Rate limit de 5 por hora por IP.
  </Step>

  <Step title="Resgatar">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/magic-link/redeem \
      -H 'Content-Type: application/json' \
      -d '{ "token": "uk_ml_…" }'
    ```

    Responde o contato e uma sessão `uk_ct_…` verificada.
  </Step>
</Steps>

O resgate é atômico em SQL, então o link funciona exatamente uma vez. Usado,
expirado e nunca existiu respondem o mesmo `401` — três verdades diferentes, uma
resposta, de propósito.

## Códigos por e-mail

O mesmo fluxo apresentado como seis dígitos, e também funciona nos **dois**
modos. Prefira o código ao link quando ele vai ser digitado no aparelho que o
pediu: um código sobrevive ao cliente de e-mail que abre links no navegador dele,
e funciona quando a caixa de entrada está no celular e o login está no notebook.

<Steps>
  <Step title="Pedir">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/email-code \
      -H 'Content-Type: application/json' \
      -d '{ "publishable_key": "uk_pk_live_…", "email": "grace@example.com" }'
    ```

    ```json 202 — sempre theme={null}
    { "challenge_token": "uk_ec_…", "expires_at": "2026-03-01T12:10:00Z" }
    ```

    Guarde o desafio na página. Ele não é uma sessão e nunca pode ser usado como
    uma. Vale por dez minutos. Rate limit de 5 por hora por IP.
  </Step>

  <Step title="Verificar">
    ```bash theme={null}
    curl -s $API/v1/contact-auth/email-code/verify \
      -H 'Content-Type: application/json' \
      -d '{ "publishable_key": "uk_pk_live_…", "challenge_token": "uk_ec_…", "code": "418902" }'
    ```

    Responde o contato e uma sessão `uk_ct_…` verificada. O código chegou na caixa de
    entrada, então ele comprova o endereço exatamente como o link.
  </Step>
</Steps>

O desafio volta para **qualquer** endereço, tendo conta ou não — do contrário
este endpoint seria justamente onde alguém descobre quais endereços têm. A
verificação mantém a mesma linha: código errado, expirado, já usado e desafio que
nunca teve código atrás dele respondem o mesmo `401 invalid_code`.

<Note>
  Duas regras são o que torna seis dígitos seguros de aceitar, e as duas importam:

  * **Cada código tem cinco tentativas.** Um palpite gasta uma, certo ou errado, e o
    código morre quando acabam. Seis dígitos são um milhão, o que o rate limit
    sozinho não fecha.
  * **Pedir um código novo gasta o anterior.** Senão cada pedido somaria mais cinco
    palpites contra mais um número.

  O código sozinho também não basta: ele só vale contra o desafio para o qual foi
  emitido, então um código lido por cima do ombro de alguém não abre nada sem o
  navegador que o pediu.
</Note>

## Política de senha

Pelo menos 8 caracteres, com uma letra minúscula, uma maiúscula e um dígito.
Qualquer coisa mais curta ou mais simples responde `400 weak_password`.

Deliberadamente modesta: comprimento é o que importa, e uma política contra a qual
as pessoas brigam produz `Password1!` em todo lugar.

**E a senha não pode ser uma senha sabidamente vazada.** Toda senha definida aqui
— cadastro, redefinição, "definir uma senha" — é conferida contra um corpus
público de senhas que apareceram em vazamentos, e um acerto responde
`400 breached_password`. Essa regra sozinha impede mais tomadas de conta que
qualquer exigência de composição, porque credential stuffing é o que de fato
acontece com os seus clientes.

<Note>
  A senha não sai do nosso processo, e nada que a identifique sai também. Ela é
  hasheada localmente e só os cinco primeiros caracteres desse hash vão na rede; o
  corpus responde com todos os candidatos sob aquele prefixo e a comparação
  acontece aqui. O serviço aprende que alguém perguntou sobre uma de várias
  centenas de senhas, e nada além disso.

  A checagem **falha aberta**. Se o corpus não puder ser alcançado a senha é
  aceita e nós registramos: recusar todo cadastro porque um terceiro caiu seria uma
  indisponibilidade que importamos para dentro do seu produto.
</Note>

## A sessão

Todo fluxo bem-sucedido aqui devolve uma sessão de contato `uk_ct_…`, válida por 30
dias, com `verified: true`. Use nas leituras do próprio contato:

```bash theme={null}
curl -s $API/v1/contact/me            -H "Authorization: Bearer uk_ct_…"
curl -s -X POST $API/v1/contact/logout -H "Authorization: Bearer uk_ct_…"
```

Sair deleta a sessão — a próxima busca simplesmente não casa com nada.
