Skip to main content
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 (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):
Resposta

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. 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.
.env.local
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

app/api/userkit/[...userkit]/route.ts
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.
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 da chave publicável; uma lista vazia continua permitindo tudo, então nada muda até você restringi-la.

Passo 3 — o provider e o boot

app/layout.tsx
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:
app/api/userkit-boot/route.ts
app/userkit-boot.tsx
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

app/dashboard/page.tsx
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:
app/api/my-data/route.ts
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.
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):
main.tsx
<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:
App.tsx
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:
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:
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:
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:
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:
app/api/userkit-webhooks/route.ts
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:
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 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, comprovar um endereço, tokens de sessão e revogação, webhooks por inteiro e a referência da API. Esta documentação também é publicada para máquinas em docs.userkit.dev/llms.txt.