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

# Tokens de sessão

> Um JWT de vida curta que o seu backend verifica offline, e a sessão opaca que continua emitindo.

Suas páginas guardam uma sessão de contato `uk_ct_…`. O seu **backend** precisa
saber quem está chamando, e perguntar para a gente a cada requisição colocaria a
nossa latência e a nossa disponibilidade na frente da sua API.

Então a sessão de contato compra um **JWT** de vida curta, assinado por ambiente,
que o seu backend verifica offline contra um conjunto de chaves públicas.

```
sua página ──uk_ct_… ──▶ POST /v1/contact/token ──▶ JWT (5 min)
                                                     │
sua página ──Authorization: Bearer <JWT> ──▶ seu backend
                                                     │
                                    verifica contra GET /v1/jwks/{publishable_key}
                                    (em cache — sem chamada nossa por requisição)
```

Duas credenciais, dois trabalhos. A sessão opaca é **de vida longa e revogável**. O
JWT é **de vida curta e irrevogável**, e é exatamente por isso que ele é curto.

## Emitindo

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

```json theme={null}
{
  "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjZmMWMifQ…",
  "expires_at": "2026-07-29T12:05:00Z"
}
```

Este é o fluxo de refresh, e não há uma segunda credencial para gerenciar: **a
sessão opaca é o refresh token**. Chame de novo sempre que o último JWT estiver
perto de expirar, enquanto a sessão viver.

<Note>
  Cada emissão registra o contato no medidor de contatos ativos do mês, de forma
  idempotente. É esse o número que a sua fatura lê — veja
  [o medidor](#o-medidor-de-contatos-ativos).
</Note>

## O que o token carrega

<ResponseField name="iss" type="string">
  O id do ambiente. **Fixe ele.** Um token do ambiente de teste não pode satisfazer
  uma verificação de produção.
</ResponseField>

<ResponseField name="sub" type="uuid">
  O id do contato.
</ResponseField>

<ResponseField name="sid" type="uuid">
  O id da sessão — o que você passa para `DELETE /v1/contact/sessions/{id}`.
</ResponseField>

<ResponseField name="external_id" type="string">
  O seu próprio id para essa pessoa, presente quando o contato tem um. No modo
  federado é nele que o seu backend se apoia.
</ResponseField>

<ResponseField name="email" type="string" />

<ResponseField name="verified" type="boolean">
  Se a identidade por trás da sessão foi **comprovada**. Barre nisso antes de
  qualquer coisa por onde os dados de outra pessoa pudessem vazar — um token com
  `verified: false` é uma assinatura perfeitamente válida sobre uma afirmação não
  comprovada.
</ResponseField>

<ResponseField name="iat / exp" type="integer">
  Segundos Unix. A validade é de 5 minutos.
</ResponseField>

## Verificando

Busque o conjunto de chaves uma vez, guarde em cache, e verifique localmente.

```bash theme={null}
curl -s $API/v1/jwks/uk_pk_live_3d8e…
```

```json theme={null}
{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
      "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
      "kid": "6f1c…",
      "alg": "ES256",
      "use": "sig"
    }
  ]
}
```

Endereçado pela **chave publicável** — o único identificador que o seu backend já
tem na config. Público e sem autenticação por definição: são chaves públicas. Também
não há portão de Origin, porque quem chama é um servidor, e servidores não mandam
Origin.

<CodeGroup>
  ```ts Node — jose theme={null}
  import { createRemoteJWKSet, jwtVerify } from "jose";

  const jwks = createRemoteJWKSet(
    new URL(`${API}/v1/jwks/${process.env.USERKIT_PUBLISHABLE_KEY}`),
  );

  export async function contactFromRequest(authorization?: string) {
    const token = authorization?.replace(/^Bearer /, "");
    if (!token) return null;

    const { payload } = await jwtVerify(token, jwks, {
      issuer: process.env.USERKIT_ENVIRONMENT_ID, // fixa o ambiente
    });
    return payload;
  }
  ```

  ```go Go — go-jose theme={null}
  set, err := jwk.Fetch(ctx, apiURL+"/v1/jwks/"+publishableKey)
  if err != nil {
  	return nil, err
  }

  claims, err := jwt.Parse(
  	[]byte(token),
  	jwt.WithKeySet(set),
  	jwt.WithIssuer(environmentID), // fixa o ambiente
  	jwt.WithValidate(true),
  )
  ```

  ```python Python — PyJWT theme={null}
  from jwt import PyJWKClient
  import jwt

  jwks = PyJWKClient(f"{API}/v1/jwks/{PUBLISHABLE_KEY}", cache_keys=True)

  def contact_from_token(token: str):
      key = jwks.get_signing_key_from_jwt(token).key
      return jwt.decode(
          token,
          key,
          algorithms=["ES256"],
          issuer=ENVIRONMENT_ID,  # fixa o ambiente
      )
  ```
</CodeGroup>

<Warning>
  Verifique a assinatura. Não leia o payload sem ela — um JWT é base64, não é
  criptografia, e qualquer coisa pode ser digitada dentro de um.
</Warning>

### Guarde o conjunto de chaves em cache

A resposta carrega:

```
Cache-Control: public, max-age=300, stale-while-revalidate=86400
```

Isso é deliberado. Um verificador precisa continuar verificando durante os nossos
cinco minutos ruins, então um CDN ou um cache em processo deve seguir respondendo
enquanto uma nova busca é tentada. A maioria dos clientes de JWKS respeita isso
sozinha; os acima respeitam.

As chaves são criadas na primeira leitura, então o documento nunca fica vazio para
um ambiente configurado. Quando a emissão de JWT está indisponível no servidor, a
resposta é um **conjunto vazio** em vez de um erro — o formato continua válido, e é
o `POST /v1/contact/token` que devolve o `501 jwt_unavailable`.

## Revogação, e os cinco minutos

Um JWT é verificado offline, então nada consegue chamá-lo de volta. Deletar uma
sessão interrompe o **próximo** refresh, não o token que já está numa página.

Isso faz de cinco minutos o tempo máximo que uma sessão revogada continua
funcionando — e é toda a razão de a credencial de vida longa ser a opaca e
revogável, e a de vida curta ser a assinada. Se você precisa de revogação imediata
para uma ação específica, confira a sessão no servidor para aquela ação, em vez de
alongar o JWT.

## Sessões que o contato enxerga

O token de sessão de contato vive no seu domínio sem `httpOnly`. Isso faz de "ver
meus dispositivos e encerrar um" parte da defesa desse token, não um extra.

<CodeGroup>
  ```bash Listar theme={null}
  curl -s $API/v1/contact/sessions -H "Authorization: Bearer uk_ct_…"
  ```

  ```bash Revogar uma theme={null}
  curl -s -X DELETE $API/v1/contact/sessions/{id} -H "Authorization: Bearer uk_ct_…"
  ```

  ```bash Sair de todos os outros theme={null}
  curl -s -X DELETE $API/v1/contact/sessions -H "Authorization: Bearer uk_ct_…"
  ```
</CodeGroup>

```json theme={null}
{
  "sessions": [
    {
      "id": "…",
      "current": true,
      "verified": true,
      "created_at": "…",
      "last_seen_at": "…",
      "expires_at": "…"
    }
  ]
}
```

Revogar é deletar — imediato, com a validade do JWT como único rastro. "Sair de
todos os outros" mantém a sessão que pediu. O id de sessão de outro contato e um
que nunca existiu respondem o mesmo `404`.

## Rotacionando a chave de assinatura

```bash theme={null}
curl -s -X POST $API/v1/organization/environments/{id}/signing-key/rotate \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

```json theme={null}
{ "environment_id": "…", "kid": "9b2d…", "grace_hours": 24 }
```

Exige `api_keys:write` — a mesma trava do segredo de identidade, porque essa chave
emite os tokens em que o seu backend confia.

A chave antiga para de assinar na hora e continua **verificando** por 24 horas: a
sobreposição que permite aos caches de JWKS se atualizarem e aos tokens em voo
expirarem. Seu backend não precisa de deploy — ele rebusca o conjunto de chaves e
encontra os dois `kid`.

Numa suspeita de comprometimento a conta corre ao contrário: todo token assinado
com a chave antiga morre em até 5 minutos da rotação. É esse o limite a citar.

## O medidor de contatos ativos

```bash theme={null}
curl -s $API/v1/organization/usage/active-contacts \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

```json theme={null}
{
  "environment_id": "…",
  "month": "2026-07",
  "active_contacts": 1842,
  "limit": 50000,
  "period_ends_at": "2026-08-01T00:00:00Z"
}
```

Exige `customers:read`. Um contato conta como ativo num mês quando um JWT é emitido
para ele — então uma sessão aberta em janeiro e renovada em fevereiro está ativa nos
dois.

**Só o ambiente live**, porque só contatos de produção contam. O mês é UTC, decidido
uma vez no schema: a exibição pode traduzir, uma fatura não pode ser ambígua. Isso
lê a mesma consulta que a fatura lê.
