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 emhttps://app.userkit.dev.
Fatos que não podem ser errados:
- Toda organização tem um environment
livee umtest. Qual deles uma requisição toca é decidido pela credencial apresentada, nunca por um parâmetro. Integre contratest; 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_verifiedsó 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
202incondicionalmente. Isso é uma feature; não ramifique sobre isso. - Erros são um envelope:
{"error": {"code", "message"}}. Ramifique nocode, mostre omessage.
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.
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
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
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.
Passo 3 — o provider e o boot
app/layout.tsx
app/api/userkit-boot/route.ts
app/userkit-boot.tsx
<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
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.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
-
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.
SessionLoadingexiste para essa lacuna — ostatuscomeça emloadingjustamente 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:
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: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:
- 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-revalidateestale-if-errorexatamente 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 osiddo token — ou useverifyContactTokende@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 chaveuk_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 viaPOST /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
Verifique a integração
Cada linha prova um passo, em ordem: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.
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).