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

# Integração por agente

> O caminho feliz inteiro numa página só — instalar, chaves, sign-in, verificação de JWT e webhooks — escrito para um agente que lê uma vez e não navega.

Esta página é a integração completa, em ordem, sem depender de nenhuma outra
página para terminar. Foi escrita para um agente de IA fazendo a integração em
nome de um desenvolvedor — o que também a torna a leitura mais rápida para uma
pessoa. Todos os comandos rodam contra o serviço hospedado em
`https://api.userkit.dev`.

## O que você está integrando

O UserKit é o plano do cliente de um produto — contatos, sessões, times,
cobrança, suporte — mais uma API de máquina para o **backend** do produto. Ele
nunca autentica os usuários do produto: o auth do próprio produto faz isso, e o
servidor do produto atesta quem está na página. As pessoas que usam o produto se chamam **contatos**. Nada aqui toca
as contas do time do desenvolvedor; essas vivem no painel em
`https://app.userkit.dev`.

Fatos que não podem ser errados:

* **Toda organização tem um environment `live` e um `test`.** Qual deles uma
  requisição toca é decidido pela credencial apresentada, nunca por um
  parâmetro. Integre contra `test`; troque as chaves para ir ao ar.
* **Quatro prefixos de credencial, quatro superfícies.** `uk_pk_…` é a chave
  publicável (identifica o environment, segura no HTML da página). `uk_sk_…` é
  a chave secreta de API (só no servidor, mostrada uma vez, nunca num bundle ou
  repositório). `uk_ct_…` é uma sessão de contato. `uk_st_…` é uma sessão de
  staff do painel — seu código nunca segura uma.
* **Um email enviado pela API é atributo, não prova.** `email_verified` só vira
  true quando um link ou código daquela caixa de entrada é usado. Não construa
  nada que assuma o contrário.
* **Os endpoints de auth nunca revelam se uma conta existe.** O magic link e
  o código por e-mail respondem `202` incondicionalmente. Isso é uma feature;
  não ramifique sobre isso.
* **Erros são um envelope**: `{"error": {"code", "message"}}`. Ramifique no
  `code`, mostre o `message`.

## Passo 0 — chaves

O desenvolvedor cria a conta em [app.userkit.dev](https://app.userkit.dev) (o
sign-up cria a organização e os dois environments numa transação). No painel,
em **Settings → API keys**, com o seletor de environment em **test**:

* copie a **chave publicável**, `uk_pk_test_…` — armazenada em texto claro e
  re-exibível;
* crie uma **chave de API**, `uk_sk_test_…` — mostrada exatamente uma vez; uma
  chave perdida se substitui, nunca se recupera.

Confirme a chave de API e anote o id do environment (necessário depois, na
verificação do JWT):

```bash theme={null}
curl -s https://api.userkit.dev/v1/me -H "Authorization: Bearer uk_sk_test_…"
```

```json Resposta theme={null}
{
  "organization": { "id": "…", "name": "Analytical Engine" },
  "environment": { "id": "…", "kind": "test" },
  "api_key": { "id": "…", "prefix": "uk_sk_test_…" }
}
```

## Dois arranjos — escolha antes do Passo 1

Existem exatamente duas formas de uma página segurar a sessão de um contato, e
a escolha decide os Passos 1 a 4. As duas são de primeira classe; a API foi
feita para as duas.

|                                     | **Proxy** — `@userkit/nextjs`                                                           | **Direto** — `@userkit/react`                                               |
| ----------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Exige                               | um servidor na sua própria origem (Next.js App Router)                                  | nada além do navegador: SPA, hospedagem estática, React Native              |
| A chave publicável                  | fica no servidor, então nunca entra no bundle                                           | vai no bundle — é exatamente para isso que ela existe                       |
| A sessão do contato                 | um cookie httpOnly que suas próprias rotas gravam: inalcançável por script              | `localStorage`, ou memória quando ele falha                                 |
| O JWT de 5 minutos                  | cunhado no seu servidor; `getToken()` no navegador recusa, de propósito                 | `getToken()` no navegador, cacheado e renovado 30s antes de expirar         |
| Origens permitidas                  | não é o que protege você — as chamadas saem do seu servidor — mas configure mesmo assim | **é** o controle sobre chamadas de navegador; configure antes do lançamento |
| Ler a sessão durante a renderização | `getSession()` num Server Component, antes de mandar um byte                            | depois da hidratação, pelos guardas e hooks                                 |

Escolha **proxy** quando já existe um servidor — ele compra a única coisa que o
direto não pode dar, um token de sessão que script nenhum lê. Escolha **direto**
quando não há servidor onde colocá-lo; uma chave publicável no HTML da página é
o desenho, não um vazamento, porque ela identifica um environment e não
autentica nada.

Os Passos 1 a 4 abaixo são o arranjo proxy. O direto são os mesmos quatro
passos e está escrito por inteiro logo depois deles. **Do Passo 5 em diante os
dois são idênticos** — o JWT, a superfície de máquina e os webhooks não sabem
nem se importam com o que a página escolheu.

## Passo 1 — instale o SDK

O arranjo recomendado para um app Next.js com App Router é `@userkit/nextjs`:
route handlers na própria origem do app seguram a sessão do contato num cookie
httpOnly, e a chave publicável nunca entra no bundle.

```bash theme={null}
npm install @userkit/nextjs
```

```bash .env.local theme={null}
USERKIT_PUBLISHABLE_KEY=uk_pk_test_…
USERKIT_SECRET_KEY=uk_sk_test_…
```

Deliberadamente **não** é `NEXT_PUBLIC_`: neste arranjo a chave fica no
servidor, onde os route handlers a colocam na requisição.

`USERKIT_SECRET_KEY` é opcional e vale a pena configurar. Ela não autentica
nada aqui — toda chamada dos handlers continua assinada pela chave publicável
ou pela sessão do próprio contato. O que ela faz é provar à UserKit que essas
requisições vêm de um **servidor**, e é isso que permite aos handlers dizerem
de qual visitante é cada uma.

Sem ela, toda chamada sai do seu deployment de um endereço só, então os limites
por IP da UserKit valem para a sua base inteira de uma vez: cinco códigos por
e-mail por hora viram o orçamento de todo mundo junto, e a linha "novo
aparelho, novo lugar" de um sign-in nomeia o seu servidor. A prova precisa ser um segredo — um header sozinho é um limite que
qualquer chamador zera a cada requisição —, então mantenha esta chave fora de
`NEXT_PUBLIC_` também. Ela é lida no servidor e vai em um header de saída;
nunca chega a uma resposta, a um cookie ou ao bundle.

## Passo 2 — monte os route handlers

```ts app/api/userkit/[...userkit]/route.ts theme={null}
import { createUserKitHandlers } from "@userkit/nextjs/handlers";

export const { GET, POST, PUT, DELETE } = createUserKitHandlers();
```

Essa é toda a metade servidor. Endpoints que respondem com um token `uk_ct_…`
têm o token retirado da resposta e gravado como cookie httpOnly; toda chamada
autenticada dali em diante é assinada aqui, não no navegador. Um `401` numa
dessas chamadas assinadas apaga o cookie, para que uma sessão revogada de outro
aparelho não deixe para trás uma credencial que não autentica nada.

<Warning>
  **A origem que vai no fio é a deste deployment, não a do navegador.** Essas
  chamadas saem do seu servidor, então o handler manda o próprio endereço como
  `Origin` — uma requisição same-origin da página não carrega nenhum para
  repassar. Coloque **a origem do seu próprio app** na [lista de origens
  permitidas](/pt-br/guides/api-keys#a-lista-de-origens-permitidas) da chave
  publicável; uma lista vazia continua permitindo tudo, então nada muda até você
  restringi-la.
</Warning>

## Passo 3 — o provider e o boot

```tsx app/layout.tsx theme={null}
import { UserKitProvider } from "@userkit/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <body>
        <UserKitProvider>{children}</UserKitProvider>
      </body>
    </html>
  );
}
```

O auth do próprio app faz o login. O que o UserKit precisa é de um **boot**
que nomeie esse usuário, assinado pelo servidor para que a página não consiga
forjar:

```ts app/api/userkit-boot/route.ts theme={null}
import { createHmac } from "node:crypto";
import { NextResponse } from "next/server";

import { currentUserId } from "@/lib/auth"; // o que o auth do app expuser

export const dynamic = "force-dynamic";

export async function GET() {
  const externalId = await currentUserId();
  if (!externalId) {
    return NextResponse.json({ error: "not_signed_in" }, { status: 401, headers: NO_STORE });
  }
  const hash = createHmac("sha256", process.env.USERKIT_IDENTITY_SECRET!)
    .update(externalId)
    .digest("hex");
  return NextResponse.json({ external_id: externalId, hash }, { headers: NO_STORE });
}

// Este corpo identifica uma pessoa; um CDN que o guardasse entregaria ao
// próximo visitante a prova da identidade de outra pessoa.
const NO_STORE = { "Cache-Control": "no-store" };
```

```tsx app/userkit-boot.tsx theme={null}
"use client";

import { useEffect } from "react";
import { useUserKit } from "@userkit/nextjs";

export function UserKitBoot() {
  const client = useUserKit();
  useEffect(() => {
    void (async () => {
      const res = await fetch("/api/userkit-boot", { cache: "no-store" });
      if (res.status === 401) {
        await client.boot(); // visitante; a atribuição de primeiro toque nasce aqui
      } else {
        const { external_id, hash } = await res.json();
        await client.boot({ externalId: external_id, hash });
      }
    })();
  }, [client]);
  return null;
}
```

Renderize `<UserKitBoot />` uma vez dentro do provider. A mensagem assinada é o
`external_id` e nada mais; `USERKIT_IDENTITY_SECRET` vem de
`GET /v1/organization/environments/{id}/identity`, por environment, e nunca sai
do servidor. Um hash válido cunha uma sessão **verificada**; um hash ausente
cunha uma sessão não verificada, boa para desenvolvimento e barrada de tudo por
onde os dados de outra pessoa poderiam vazar; um hash errado é recusado com
`401 invalid_identity_hash` em vez de rebaixado. A regra inteira está em
`/pt-br/customer-auth/federated`, e as páginas de Supabase, Clerk, Firebase e
Better Auth ao lado mostram de onde vem o `currentUserId` em cada um.

Guardas para componentes de cliente: `SignedIn`, `SignedOut`,
`SessionLoading`, `Verified`, e os hooks `useContact()` / `useSession()`, todos
importados de `@userkit/nextjs`.

## Passo 4 — proteja uma página

```tsx app/dashboard/page.tsx theme={null}
import { getSession } from "@userkit/nextjs/server";
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const session = await getSession();
  if (!session) redirect("/sign-in");

  return <p>Conectado como {session.contact.email}</p>;
}
```

`getSession()` é memoizado por requisição — chamá-lo num layout e em três
componentes custa uma ida só. Responde `{ contact, verified, expiresAt }` ou
`null`. Nos route handlers e Server Actions do próprio app, a mesma chamada é a
autenticação:

```ts app/api/my-data/route.ts theme={null}
import { getSession } from "@userkit/nextjs/server";

export async function GET() {
  const session = await getSession();
  if (!session?.verified) {
    return Response.json({ error: { code: "unauthorized" } }, { status: 401 });
  }
  return Response.json({ contactId: session.contact.id });
}
```

Condicione em `verified`, não só na presença: uma sessão não verificada é uma
sessão válida sobre uma alegação de identidade não provada.

## Passos 1 a 4, arranjo direto

Pule esta seção se o app ficou no proxy acima. Estes são os mesmos quatro
passos para um app sem servidor próprio — uma SPA em Vite, um export estático,
React Native. Nada depois disso muda.

```bash theme={null}
npm install @userkit/react
```

Aqui a chave é pública, então ela viaja como configuração pública viaja
(`VITE_USERKIT_PUBLISHABLE_KEY`, `NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY`, uma
constante de build — o que o bundler já fizer):

```tsx main.tsx theme={null}
import { UserKitProvider } from "@userkit/react";

createRoot(document.getElementById("root")!).render(
  <UserKitProvider publishableKey={import.meta.env.VITE_USERKIT_PUBLISHABLE_KEY}>
    <App />
  </UserKitProvider>,
);
```

`<UserButton />`, os guardas e o boot são os mesmos, importados de
`@userkit/react` — a metade de servidor do boot é o endpoint que o backend do
app expuser para assinar o id:

```tsx App.tsx theme={null}
import { SignedIn, SignedOut, SessionLoading, useContact } from "@userkit/react";

function App() {
  return (
    <>
      {/* o mesmo <UserKitBoot /> do passo Next.js acima */}
      <UserKitBoot />
      <SessionLoading>
        <Spinner />
      </SessionLoading>
      <SignedOut>
        <a href="/login">Entrar</a> {/* o login do próprio app */}
      </SignedOut>
      <SignedIn>
        <Dashboard />
      </SignedIn>
    </>
  );
}

function Dashboard() {
  const contact = useContact(); // null até existir uma sessão
  return <p>Conectado como {contact?.email}</p>;
}
```

Duas diferenças em relação ao arranjo proxy, e elas são a diferença inteira:

* **Não existe leitura de sessão no servidor**, então uma tela protegida é um
  guarda renderizado, não um redirect decidido antes da resposta.
  `SessionLoading` existe para essa lacuna — o `status` começa em `loading`
  justamente para que um botão de sign-in não pisque a cada recarga.
* **O token é obtido no navegador.** `useUserKit()` entrega o mesmo client que
  os componentes usam:

  ```ts theme={null}
  import { useUserKit } from "@userkit/react";

  const client = useUserKit();
  const token = await client.getToken(); // cacheado, renovado 30s antes de expirar
  ```

Antes de este app ir para produção, configure as **origens permitidas** da
chave publicável no painel. No arranjo direto essa lista é a única coisa entre
a chave na sua página e a mesma chave na página de outra pessoa.

## Passo 5 — um backend em outra linguagem

Um backend que não é o servidor Next verifica um **JWT** de vida curta,
offline — nenhuma chamada ao UserKit no caminho da requisição.

De onde vem o JWT é o único ponto em que os dois arranjos ainda diferem, e a
diferença é um import:

<CodeGroup>
  ```ts Direto — no navegador theme={null}
  import { useUserKit } from "@userkit/react";

  const client = useUserKit();
  const token = await client.getToken(); // cacheado, renovado 30s antes de expirar
  await fetch("https://api.seu-backend.com/things", {
    headers: { Authorization: `Bearer ${token}` },
  });
  ```

  ```ts Proxy — no seu servidor theme={null}
  import { getToken } from "@userkit/nextjs/server";

  const token = await getToken(); // null quando não há cookie de sessão
  await fetch("https://api.seu-backend.com/things", {
    headers: { Authorization: `Bearer ${token}` },
  });
  ```
</CodeGroup>

Em modo proxy, `getToken()` no navegador lança `unsupported` em vez de entregar
uma credencial ao script — o cookie está no seu servidor, então a cunhagem
também está.

O token é cunhado por `POST /v1/contact/token` a partir da sessão `uk_ct_…`, é
assinado com **ES256 por environment** e vive **5 minutos** — que é o que torna
seguro cachear o conjunto de chaves de verificação. Claims: `iss` (o id do
environment — fixe-o), `sub` (id do contato), `sid` (id da sessão),
`external_id`, `email`, `verified`, `iat`/`exp`.

O backend verifica contra o conjunto público de chaves, endereçado pela chave
publicável que a configuração do backend já tem:

```bash theme={null}
curl -s https://api.userkit.dev/v1/jwks/uk_pk_test_…
```

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

  const jwks = createRemoteJWKSet(
    new URL(`https://api.userkit.dev/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, // do /v1/me, passo 0
    });
    return payload; // condicione em payload.verified antes de confiar na identidade
  }
  ```

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

  jwks = PyJWKClient(
      f"https://api.userkit.dev/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)
  ```
</CodeGroup>

Regras que sustentam isso:

* **Verifique a assinatura antes de ler qualquer coisa.** JWT é base64, não
  criptografia.
* **Fixe o issuer.** Um token do environment de teste não pode satisfazer uma
  checagem de produção.
* **Cacheie o conjunto de chaves e continue verificando quando o refetch
  falhar.** A resposta carrega `stale-while-revalidate` e `stale-if-error`
  exatamente para isso; os clientes acima respeitam.
* Para revogação abaixo dos 5 minutos, consulte
  `GET /v1/revocations/{publishable_key}` (cacheável, `max_age_seconds: 15`) e
  compare com o `sid` do token — ou use `verifyContactToken` de
  `@userkit/nextjs/verify`, que faz assinatura e revogação numa chamada
  offline. Um verificador que não consegue buscar a lista continua
  verificando; a expiração do próprio token é o piso.

## Passo 6 — identifique contatos do servidor

A superfície de máquina, com a chave `uk_sk_…`. Identify é create-or-update:

```bash theme={null}
curl -s https://api.userkit.dev/v1/contacts \
  -H "Authorization: Bearer uk_sk_test_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 018f3b2e-user-8421-created" \
  -d '{
    "external_id": "user_8421",
    "email": "grace@example.com",
    "name": "Grace Hopper",
    "customer": { "external_id": "org_77", "name": "Acme" }
  }'
```

`201` com `"created": true` na primeira vez, `200` com `"created": false`
depois. Toda escrita nesta superfície aceita um `Idempotency-Key` e responde
uma repetição com o mesmo status e o mesmo corpo — programas repetem, então
mande um em toda escrita. Os limites de taxa são por chave, com teto por
environment, e toda resposta carrega os headers `RateLimit-*`; regule o ritmo
por eles em vez de tropeçar no `429`.

## Passo 7 — webhooks

Registre um endpoint no painel (**Webhooks**, escolhendo o environment), ou
com as credenciais do próprio painel via `POST /v1/organization/webhooks`. Um
endpoint é uma URL (`https://` apenas), um segredo de assinatura (re-exibível
no painel) e uma lista de tipos de evento inscritos — vazia significa todos.

O contrato do handler: a entrega é **at-least-once e sem ordem**. Deduplique
pelo `id`, ordene pelo `sequence`, responda `2xx` em até dez segundos e faça o
trabalho depois. Verifique a assinatura sobre os **bytes crus**:

```ts app/api/userkit-webhooks/route.ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const header = request.headers.get("UserKit-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));

  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  const expected = createHmac("sha256", process.env.USERKIT_WEBHOOK_SECRET!)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const valid =
    age < 300 &&
    !!parts.v1 &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(String(parts.v1)));
  if (!valid) return new Response("invalid signature", { status: 400 });

  const event = JSON.parse(rawBody);
  // event.id       → deduplique por ele antes de agir
  // event.type     → ex.: "contact.identified", "contact.signed_in"
  // event.sequence → maior = fato mais recente; nunca compare ordem de chegada
  // event.data     → ids, não snapshots — releia o recurso para o estado atual

  return new Response(null, { status: 202 });
}
```

O timestamp está dentro do que é assinado, e recusar os antigos é a sua metade
da defesa contra replay. Entregas que falham são repetidas doze vezes ao longo
de \~14 horas; cinco entregas seguidas gastando todas as tentativas desligam o
endpoint e mandam email aos owners da organização.

## Verifique a integração

Cada linha prova um passo, em ordem:

```bash theme={null}
# as chaves são reais e nomeiam o environment de teste
curl -s https://api.userkit.dev/v1/me -H "Authorization: Bearer uk_sk_test_…"

# a configuração pública do environment — marca, idioma, o que o widget desenha
curl -s https://api.userkit.dev/v1/config/uk_pk_test_…

# o conjunto de chaves que o backend vai usar para verificar (nunca vazio depois de configurado)
curl -s https://api.userkit.dev/v1/jwks/uk_pk_test_…

# um contato cunhado pelo servidor existe e pode ser lido de volta
curl -s https://api.userkit.dev/v1/contacts \
  -H "Authorization: Bearer uk_sk_test_…" -H "Content-Type: application/json" \
  -d '{ "external_id": "smoke_test_1",
       "customer": { "external_id": "org_smoke", "name": "Smoke" } }'
```

Depois, num navegador: entre pelo auth do próprio app, confirme que o boot
responde `verified: true`, confirme que a página protegida desenha o contato e
confirme que o contato aparece no painel no environment de **test**. Para webhooks, a tela de
endpoints do painel tem um envio de teste (`webhook.test`) e um log de entregas
com replay.

### Pergunte ao doctor antes de dizer que funciona

As linhas de curl acima provam que as peças respondem. O **install doctor** é a
pergunta que nenhuma delas responde sozinha — este environment está de fato
ligado — e ele reporta cada verificação com a evidência por trás dela e o
conserto quando há um:

* no painel, em **Organização → início**;
* como a ferramenta `run_doctor`, se o assistente que está fazendo esta
  integração tiver o [servidor MCP](/pt-br/guides/mcp) conectado. Ela não aceita
  argumento nenhum: o environment é o da chave de API.

São **três** status. `unknown` quer dizer que a verificação não teve como saber —
não que ela passou — e uma verificação que não se aplica a este environment fica
de fora em vez de responder `ok`. Um resumo que dobra os `unknown` em "está tudo
funcionando" é exatamente a falha que este endpoint existe para evitar, porque uma
integração não testada e uma integração quebrada são iguais vistas daqui.

Ir ao ar é uma troca de chaves: crie as chaves live, troque
`uk_pk_test_…`/`uk_sk_test_…` pelos equivalentes `live` no ambiente do deploy
e — antes do lançamento — configure as **origens permitidas** da chave
publicável no painel, o que restringe chamadas de navegador aos domínios do
próprio produto (uma lista vazia permite qualquer origem, que é o padrão certo
para localhost e o errado para produção).

## Onde vive o resto

Tudo acima é suficiente para lançar. As páginas mais profundas, quando uma
superfície específica importar:
[identidade federada](/pt-br/customer-auth/federated),
[comprovar um endereço](/pt-br/customer-auth/email-proof),
[tokens de sessão e revogação](/pt-br/customer-auth/session-tokens),
[webhooks por inteiro](/pt-br/guides/webhooks) e a
[referência da API](/pt-br/api-reference/introduction). Esta documentação
também é publicada para máquinas em
[docs.userkit.dev/llms.txt](https://docs.userkit.dev/llms.txt).
