API Keys
ProduçãoCredencial programática de longa duração, com escopo por rota e rotação sem queda
Seu parceiro integra por API com uma credencial que você consegue limitar a rotas específicas, rotacionar sem derrubar a integração e revogar quando quiser — e você vê quantas chamadas ele fez, por endpoint.
- Plataformas B2B cujos clientes e parceiros consomem a API sem interface humana no meio
- Times que hoje distribuem usuário de serviço com senha fixa em variável de ambiente
- Operações que precisam medir consumo por integração para cobrar ou para dimensionar
- Usuário de serviço com senha em variável de ambiente e permissão de administrador
- Gestão de chaves comprada à parte (Unkey, Zuplo) ou o plano de uso de um gateway
- Planilha de controle de quem tem qual credencial e desde quando
- Um API gateway — não faz roteamento, cache, transformação nem balanceamento
- Um cofre de segredos: guarda o hash da chave, nunca a chave
- Substituto do IAM para integração viva: escopo dinâmico e token curto continuam sendo client_credentials do IAM
15 endpoints em 3 recursos.
Resumo executivo
O API Keys emite a credencial que um sistema usa para chamar a sua API quando não há pessoa nenhuma na frente da tela. Ela é criada por quem tem permissão, exibida uma única vez, e a partir daí só existe como hash no banco.
O que muda na prática: em vez de entregar ao parceiro um usuário de serviço com senha eterna e permissão de administrador, você entrega uma chave que só responde em GET /commerce/api/v1/orders/**, aceita 500 chamadas por minuto, expira numa data e aparece num relatório de uso por endpoint. Quando o contrato acaba, uma chamada revoga.
Está em produção desde novembro de 2025 e é parte do authMiddleware compartilhado: qualquer building block da plataforma aceita autenticação por chave de API, sem que nenhum deles precise saber que o módulo existe.
| Atributo | Valor |
|---|---|
| Identificador | api-keys |
| Categoria | Identidade |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3012 |
| Path alias | @api-keys |
| Prefixo HTTP | /api-keys |
| Status | Produção desde 2025-11 |
| Depende de | PostgreSQL, Redis, IAM |
O problema
negócioO cenário. Sua plataforma tem API, e alguém do outro lado precisa chamá-la sem passar por tela de login: o ERP do cliente, o marketplace parceiro, o job noturno que concilia. Esse alguém precisa de uma credencial que dure, porque ninguém vai digitar senha às três da manhã.
O caminho mais curto é criar um usuário chamado integracao@empresa.com e mandar a senha por e-mail. Ele funciona por muito tempo — e o problema aparece quando você tenta desfazer.
flowchart TD P["Parceiro precisa chamar a API"] --> U["Usuário de serviço<br/>integracao@empresa.com"] U --> S1["Senha em variável de ambiente<br/>do sistema do parceiro"] U --> S2["Permissão de administrador,<br/>porque ninguém quis<br/>descobrir o mínimo"] U --> S3["Sem data de validade<br/>e sem registro de uso"] S1 --> R["Revogar quebra produção<br/>às três da tarde"] S2 --> R S3 --> R
O que trava hoje.
- A credencial não tem escopo. O parceiro que só precisa consultar pedidos recebe uma senha que também cancela pedidos. Descobrir o conjunto mínimo dá trabalho, então ninguém faz.
- Revogar é evento traumático. Trocar a senha derruba a integração no mesmo instante. Sem janela de transição, a operação vira combinado por telefone e madrugada de plantão.
- Não dá para saber quem usou o quê. Uma senha compartilhada entre três sistemas do parceiro não responde qual deles gerou o pico de ontem.
- A credencial vaza no lugar mais comum do mundo. Chave hardcoded em repositório é a origem clássica de incidente — tanto que o GitHub mantém um programa de parceria em que provedores registram o padrão dos próprios tokens para que o scanner os detecte e notifique (GitHub Docs — About secret scanning, consultado em 2026-08-17). Credencial sem formato reconhecível fica de fora desse tipo de rede de proteção.
- Chave de gateway não é autorização, e a própria AWS avisa. A documentação do API Gateway é literal: "Don't use API keys for authentication or authorization to control access to your APIs. If you have multiple APIs in a usage plan, a user with a valid API key for one API in that usage plan can access all APIs in that usage plan." (AWS Docs — Usage plans and API keys, consultado em 2026-08-17). Quem trata a chave do gateway como controle de acesso está construindo em cima de um aviso explícito do fornecedor.
O custo de não resolver. O custo não aparece em fatura, aparece em incidente. Uma credencial sem escopo, sem expiração e sem rastreio é uma falha de controle de acesso esperando uma oportunidade — e "Broken Access Control" lidera o OWASP Top 10 desde 2021 (OWASP Top 10:2021). O segundo custo é operacional: cada rotação de senha compartilhada é uma janela de manutenção negociada.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Usuário de serviço com senha em variável de ambiente | Chave prefixo.segredo, com o segredo guardado só como hash Argon2id |
| Permissão de administrador porque ninguém quis descobrir o mínimo | allowedResources no formato GET /caminho/**, verificado a cada requisição |
| Rotacionar derruba a integração na hora | Rotação mantém o prefixo e aceita as duas chaves por até 168 horas |
| "Quem chamou a API ontem?" é uma suposição | Uso agregado por hora e por endpoint, consultável por período |
| A credencial não expira nunca | expiresAt opcional, verificado na autenticação |
| Limite de vazão é global ou não existe | rateLimitMax e rateLimitWindowMs por chave |
O que sustenta a tabela é a separação entre quem identifica e o que autentica: o prefixo é público e serve para achar a chave; o segredo nunca é guardado.
flowchart LR
K["Chave completa<br/>prefixo.segredo"] --> P["prefixo — 8 caracteres<br/>gravado em claro<br/>é o índice de busca"]
K --> S["segredo — 32 caracteres<br/>hash Argon2id<br/>o texto puro nunca é gravado"]
P --> DB[("api_keys")]
S --> DB
K -.->|"exibida uma única vez,<br/>na criação e na rotação"| C["Quem criou"]Escopo por rota, não por papel
allowedResources aceita padrões como GET /commerce/api/v1/orders/** ou * /customers/api/v1/persons/*. O middleware de permissão troca a checagem de RBAC por essa comparação quando a requisição chega com chave. A chave que só lê não escreve, e isso não depende de o desenvolvedor lembrar.
Rotação com período de graça
POST /:keyId/rotate gera um segredo novo com o mesmo prefixo e guarda o hash antigo em ApiKeyRotation até gracePeriodEnds. Durante a janela — de 1 a 168 horas, você escolhe — as duas chaves autenticam. O parceiro migra no tempo dele.
Limite de vazão por chave
rateLimitMax e rateLimitWindowMs são campos da chave, não configuração global. O parceiro que faz varredura noturna e o que consulta pontualmente podem ter limites diferentes na mesma organização.
Medição de uso por endpoint
Cada requisição autenticada por chave incrementa um balde horário em ApiKeyUsageRecord, com detalhamento por MÉTODO /caminho. GET /:keyId/usage devolve o período com total e recorte por endpoint.
Verificação com cache, sem abrir mão do hash
O Argon2id é caro de propósito — 64 MB de memória e 3 passes. Verificar isso a cada requisição inviabilizaria volume. A chave verificada fica no Redis por API_KEY_CACHE_TTL_SECONDS (300 por padrão) sem guardar segredo em lugar nenhum: o que vai para o cache é o hash e, depois que o Argon2 aceitou um segredo, o HMAC-SHA256 dele com a chave do servidor (JWT_SECRET). Enquanto a entrada vive, o mesmo segredo é reconhecido pelo HMAC em microssegundos; o Argon2 só roda na primeira chamada de cada janela, para segredo diferente e para chave em rotação (a carência é conferida a cada chamada). Lembrar o HMAC não estende a vida da entrada (KEEPTTL).
Medido em 18/09/2026: antes disso o cache só poupava a leitura do banco e o Argon2 rodava em toda requisição — ~0,4 s por chamada, o custo fixo de cada ida do painel a qualquer bloco.
Casos de uso reais
negócioCaso 1 — Um marketplace consulta pedidos e não consegue cancelar nenhum Cenário ilustrativo
Varejista que expõe o catálogo e o status de pedidos para três marketplaces parceiros. Cada um consulta várias vezes por minuto.
Os três recebiam o mesmo usuário de serviço, com permissão ampla, porque separar exigia descobrir o conjunto exato de permissões de cada um. No papel, qualquer um dos três podia cancelar pedido. Nunca aconteceu — o que é sorte, não controle.
Uma chave por marketplace, cada uma com allowedResources restrito à leitura, rateLimitMax dimensionado ao contrato e expiresAt na data de renovação.
curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Marketplace Alfa — leitura",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-01-31T23:59:59.000Z"
}'curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Marketplace Alfa — leitura",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-01-31T23:59:59.000Z"
}'Um POST ou DELETE vindo dessa chave recebe 403 antes de chegar à regra de negócio, e o evento vai para o log de segurança com o prefixo da chave. O risco deixa de depender de ninguém ter errado.
Caso 2 — Uma rotação de credencial deixa de ser janela de madrugada Cenário ilustrativo
Financeira que precisa rotacionar as credenciais de integração a cada 90 dias por política interna de segurança.
Com senha compartilhada, rotacionar significava marcar horário com o parceiro, trocar dos dois lados no mesmo minuto e torcer. Como isso dói, a política era descumprida na prática: a rotação era adiada até virar auditoria.
POST /:keyId/rotate com gracePeriodHours combinado. A resposta traz a chave nova e o gracePeriodEnds.
sequenceDiagram
autonumber
participant Op as Operador
participant AK as API Keys
participant Pa as Sistema do parceiro
Op->>AK: POST /:keyId/rotate { gracePeriodHours 72 }
AK->>AK: novo segredo, mesmo prefixo
AK->>AK: grava hash antigo em ApiKeyRotation<br/>com gracePeriodEnds
AK-->>Op: { key nova, gracePeriodEnds }
Op->>Pa: entrega a chave nova
Note over Pa,AK: durante 72h as duas autenticam
Pa->>AK: chamadas com a chave antiga
AK-->>Pa: 200 (hash de rotação ainda válido)
Pa->>AK: chamadas com a chave nova
AK-->>Pa: 200 (hash atual)
Note over AK: passado gracePeriodEnds,<br/>só a nova respondeA rotação vira tarefa de horário comercial. A política de 90 dias passa a ser cumprível, o que é a diferença entre política e documento.
Caso 3 — O consumo de cada integração vira número Cenário ilustrativo
Plataforma que quer cobrar por volume de chamadas e, antes disso, precisa saber qual é o volume.
Com credencial compartilhada, "quantas chamadas o cliente X fez em julho" não tinha resposta confiável. O log do balanceador respondia por IP, e o parceiro tinha três IPs que mudavam.
Toda requisição autenticada por chave é contabilizada num balde horário, com detalhamento por endpoint. A consulta é por período.
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=2026-07-01&endDate=2026-07-31" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {totalRequests, totalErrors}'curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=2026-07-01&endDate=2026-07-31" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {totalRequests, totalErrors}'O consumo por integração passa a ser dado, com granularidade horária e recorte por endpoint. Isso alimenta tanto a conversa comercial quanto o dimensionamento de capacidade.
Caso 4 — Prefixo identificável é o que permite detectar vazamento Referência de mercado
O Stripe é a referência de mercado em formato de chave. As chaves dele começam com prefixos declarados — sk_live_, sk_test_, rk_live_, pk_live_ — e a documentação é explícita sobre a chave ser exibida uma única vez: "Salve o valor da chave. Não é possível recuperá-lo mais tarde." (Stripe Docs — Chaves de API, consultado em 2026-08-17).
Chave vazada em repositório público é uma das formas mais comuns de comprometimento. A resposta da indústria depende de a chave ser reconhecível por padrão: o GitHub mantém um programa de parceria em que provedores registram o formato dos próprios tokens, e o scanner notifica o provedor diretamente quando encontra um (GitHub Docs, consultado em 2026-08-17).
A chave tem a forma prefixo.segredo: 8 caracteres de prefixo e 32 de segredo, separados por ponto. O prefixo é gravado em claro e é o que aparece em log de segurança, então rastrear uma chave suspeita não exige a chave. O Stripe também documenta rotação com janela — "tanto a chave antiga quanto a nova continuam funcionando por até 7 dias" —, que é o mesmo mecanismo do nosso período de graça, cujo máximo é 168 horas, exatamente 7 dias.
Atenção. O formato da Catalisa não usa o prefixo semântico do Stripe. Não há sk_live_, não há distinção entre ambiente de teste e produção embutida na chave, e o padrão não está registrado em nenhum programa de detecção de segredos. É diferença real, e está na §15.
O que a Catalisa entrega hoje é a parte que depende de nós — prefixo público para rastreio, segredo com hash, exibição única e rotação com janela. O que depende de registro externo ainda não está feito.
Mercado e diferenciais
negócioPanorama. Gestão de chave de API é vendida de três formas. A primeira é como serviço especializado — Unkey é o exemplo mais puro —, com verificação de latência baixa cobrada por chamada válida. A segunda é como recurso de um API gateway, caso do Zuplo e do Kong Konnect: a chave vem junto com roteamento, portal do desenvolvedor e o preço do gateway. A terceira é como item de nuvem, os usage plans do AWS API Gateway, já embutidos na fatura de quem está lá.
O API Keys da Catalisa não compete com nenhum dos três no eixo em que eles são fortes. Ele não roteia, não transforma e não tem portal. Ele resolve o problema mais estreito de emitir, escopar, rotacionar, revogar e medir uma credencial que já nasce amarrada à organização do IAM.
| Critério | Catalisa API Keys | Unkey | Zuplo | AWS API Gateway |
|---|---|---|---|---|
| Modelo de preço | Incluso na plataforma | Por verificação válida/mês, com camada gratuita de 150 mil | Por requisição/mês, gratuito até 100 mil | Por milhão de chamadas |
| Escopo por rota e método | Sim, padrão MÉTODO /caminho | Sim, permissões e roles | Via política do gateway | Por plano de uso, não por rota |
| Rotação com período de graça | Sim, 1 a 168 horas | Sim | Depende da política | Não nativamente |
| Limite de vazão por chave | Sim, rateLimitMax por janela | Sim | Sim | Sim, quota e throttling |
| Uso por endpoint | Sim, balde horário com detalhamento | Sim, analytics como produto | Sim, analytics por plano | CloudWatch |
| Hash do segredo | Argon2id | Gerenciado pelo fornecedor | Gerenciado pelo fornecedor | Gerenciado pelo fornecedor |
| Amarração ao tenant | organizationId vem do token do IAM | Você modela | Você modela | Você modela |
| Roteamento, cache, transformação | Não | Não | Sim | Sim |
| Portal do desenvolvedor | Não | Parcial | Sim | Via developer portal |
| Dependência de rede externa por requisição | Não, verificação local | Sim, chamada ao serviço | Sim, é o gateway | Sim, é o gateway |
Preços conforme as páginas públicas de Unkey, Zuplo e Kong, e a documentação da AWS, consultadas em 2026-08-17. Todos variam por volume e negociação.
Nossos diferenciais
- A chave já nasce com o tenant amarrado.
organizationIdvem do token do IAM que criou a chave, não de um campo do corpo. Não existe estado em que uma chave esteja apontando para a organização errada, porque ela nunca é informada. Um serviço externo de gestão de chaves não tem como fazer isso: ele não conhece o seu conceito de organização. - A verificação não sai da sua infraestrutura. O
authMiddlewareverifica a chave contra Postgres e Redis locais. Não há chamada a terceiro no caminho de nenhuma requisição autenticada — o que elimina tanto a latência quanto o modo de falha de "o fornecedor caiu e minha API parou de autenticar". - O escopo é escrito na linguagem da sua API.
GET /commerce/api/v1/orders/**é a rota real. Não há mapeamento entre um conceito de escopo do fornecedor e o seu roteamento, que é onde esse tipo de configuração costuma divergir da realidade com o tempo.
Quando escolher o concorrente. Na maior parte dos casos em que o problema é gateway, e não credencial, a escolha certa não é o API Keys.
flowchart TD
Q1{"Você precisa de roteamento,<br/>cache, transformação ou<br/>portal do desenvolvedor?"}
Q1 -->|sim| Z["Zuplo ou Kong"]
Q1 -->|não| Q2{"Sua API está fora da<br/>plataforma Catalisa?"}
Q2 -->|sim| U["Unkey"]
Q2 -->|não| Q3{"Já vive no AWS API Gateway<br/>e precisa só de quota<br/>e throttling?"}
Q3 -->|sim| A["AWS usage plans"]
Q3 -->|não| C["Catalisa API Keys"]| Se o seu caso é | Escolha | Por quê |
|---|---|---|
| Precisa de gateway completo, com portal do desenvolvedor e transformação de requisição | Zuplo ou Kong | O API Keys não roteia nada; ele autentica |
| Sua API não é da plataforma Catalisa | Unkey | Nosso módulo verifica chave para os building blocks, não para uma API arbitrária de terceiro |
| Já usa AWS API Gateway e precisa só de quota e throttling | AWS usage plans | Está incluso e não exige infraestrutura nova — mas leia o aviso da própria AWS citado na §2 antes de tratar a chave como autorização |
| Precisa de credencial escopada, rotacionável e medida, dentro do catálogo Catalisa | Catalisa API Keys | É exatamente o recorte implementado |
Modelo de cobrança e ROI
negócioUnidade de cobrança. Precificação em definição. O API Keys é infraestrutura do authMiddleware compartilhado — todo building block aceita autenticação por chave sem contratar nada a mais. O que ainda não está fechado é se e como o volume de chamadas autenticadas por chave entra na conta.
O que dispara custo.
| Driver | Por que importa | Ordem de grandeza |
|---|---|---|
| Chaves ativas por organização | Uma linha em api_keys e uma chave no Redis por prefixo verificado | Custo de armazenamento desprezível |
| Chamadas autenticadas por chave | Cada uma consulta o cache e compara o HMAC do segredo; a verificação Argon2id (64 MB, 3 passes, ~0,4 s) roda uma vez por API_KEY_CACHE_TTL_SECONDS | O custo real é CPU e memória na verificação fria |
| Registros de uso | Um balde por chave por hora, com detalhamento por endpoint | Cresce com chaves × horas, não com chamadas |
Comparação de custo — cenário: plataforma com 25 integrações ativas fazendo, no conjunto, 5 milhões de chamadas autenticadas por mês.
| Catalisa API Keys | Unkey | Zuplo | AWS API Gateway | |
|---|---|---|---|---|
| Base de cálculo | Incluso na plataforma | Verificações válidas/mês | Requisições/mês | Milhões de chamadas REST |
| Faixa pública de referência | — | Camada gratuita até 150 mil; planos de US$ 25 a US$ 1.000/mês cobrindo de 250 mil a 100 milhões | Gratuito até 100 mil; Builder US$ 25/mês até 1 milhão; Enterprise a partir de US$ 1.000/mês | Por milhão de chamadas, conforme a região |
| 5 milhões de chamadas | Sem linha adicional | Dentro de um plano intermediário | Acima do Builder | Cobrado por volume |
| Latência adicionada | Nenhuma chamada externa | Chamada ao serviço por verificação | É o gateway | É o gateway |
Estimativa para orientar conversa, não proposta comercial. Faixas conforme as páginas públicas de Unkey e Zuplo, consultadas em 2026-08-17. Preços mudam por volume, região e negociação; confirme na data da sua análise.
ROI. A conta de guardanapo tem duas linhas. A primeira é a licença que não entra: um serviço de verificação cobrado por chamada vira custo variável que acompanha o crescimento da sua API. A segunda, mais concreta no curto prazo, é o incidente que não acontece — a credencial escopada é a diferença entre "o parceiro podia cancelar pedido e nunca cancelou" e "o parceiro não consegue cancelar pedido". Some a isso a rotação que deixa de exigir janela de manutenção negociada com cada integrador, e o retorno aparece antes de qualquer linha de fatura.
Arquitetura
As camadas e o caminho da requisição
flowchart TD
HTTP["HTTP"] --> HONO
subgraph HONO["Hono app — basePath('/api-keys')"]
R1["/api/v1/api-keys<br/>apiKeysRouter — 8 rotas"]
R2["/health"]
end
HONO -->|"Zod parse → ResultAsync<T, AppError>"| SVC
subgraph SVC["services/"]
S1["ApiKeyService<br/>criar, atualizar, rotacionar,<br/>revogar, excluir"]
S2["ApiKeyAuthService<br/>autenticar uma chave"]
S3["ApiKeyUsageService<br/>agregar e consultar uso"]
end
SVC --> REPO
subgraph REPO["repositories/ (Prisma)"]
RP1["ApiKeyRepository"]
RP2["ApiKeyRotationRepository"]
RP3["ApiKeyUsageRepository"]
end
REPO --> PG[("PostgreSQL — schema apikeys")]
S2 --> RD[("Redis — api-key:{prefixo}<br/>api-key-rotation:{id}")]
S1 --> PW["PasswordService — Argon2id"]
S2 --> PWA autenticação por chave, do outro lado
O ponto que mais surpreende quem chega ao módulo: as 8 rotas acima não são o caminho principal de uso. O uso principal é o authMiddleware compartilhado, que roda em todos os building blocks e chama o ApiKeyAuthService diretamente.
sequenceDiagram
autonumber
participant Cli as Sistema do parceiro
participant BB as Qualquer building block
participant MW as authMiddleware (compartilhado)
participant AS as ApiKeyAuthService
participant RD as Redis
participant PG as PostgreSQL
Cli->>BB: GET /commerce/api/v1/orders<br/>X-API-Key prefixo.segredo
BB->>MW: intercepta
MW->>MW: sem Bearer? extrai a chave do header
MW->>AS: authenticate(chave, método, caminho)
AS->>AS: parseFullKey → prefixo + segredo
AS->>RD: GET api-key:{prefixo}
alt cache quente
RD-->>AS: hash, status, escopos, limites
else cache frio
AS->>PG: findByPrefix(prefixo)
PG-->>AS: registro da chave
AS->>RD: SET api-key:{prefixo} (TTL)
end
AS->>AS: verify Argon2id(segredo, hash)
AS->>AS: status ACTIVE? expiresAt no futuro?
AS->>AS: isResourceAllowed(escopos, método, caminho)
AS->>AS: rate limit da chave, se configurado
AS-->>MW: ApiKeyContext (organizationId, userId, prefixo)
MW->>MW: c.set('apiKey') e c.set('user')
MW->>BB: segue para o handler
BB-->>Cli: resposta com dados só desta organizaçãoDecisões não óbvias.
- O prefixo é o índice, o segredo é o segredo. Guardar o hash da chave inteira obrigaria a testar cada chave do banco a cada requisição, porque hash não é pesquisável. Separar em prefixo público e segredo com hash resolve isso: uma busca por índice e uma verificação.
- O prefixo evita caracteres ambíguos. O alfabeto é
abcdefghjkmnpqrstuvwxyz23456789— sem0,o,1,lei. O prefixo aparece em log e em ticket de suporte, e é lido por gente. - Argon2id, e não bcrypt. 64 MB de memória e 3 passes, o mesmo
PasswordServiceusado nas senhas do IAM. É caro de propósito, e é por isso que existe cache. - O cache guarda o hash, não a chave.
CachedApiKeycarregasecretHash, status, escopos e limites. Um dump do Redis não devolve chave nenhuma; devolve o mesmo que um dump do Postgres devolveria. - A rotação reusa o prefixo. Trocar o prefixo obrigaria o parceiro a atualizar tudo de uma vez, que é exatamente o problema que o período de graça existe para evitar. Manter o prefixo é o que permite as duas chaves conviverem sob o mesmo índice.
- O
requirePermissionmuda de régua quando a autenticação é por chave. Com JWT, ele compara a permissão exigida contratoken.permissions. Com chave, ele ignora o RBAC e compara método e caminho contraallowedResources. São dois modelos de autorização diferentes no mesmo middleware, e é importante saber disso ao escopar uma chave. allowedResourcesvazio significa acesso total.isResourceAlloweddevolvetruepara lista nula ou vazia. É o padrão de menor atrito e o de maior risco — sempre preencha.- A contabilização de uso é fire and forget. O middleware dispara
trackUsagesem esperar. Falha em contabilizar não derruba a requisição; a contrapartida é que a medição é aproximada sob falha do banco.
Monolito vs. standalone. O prefixo é /api-keys nos dois modos, porque o basePath está no app.ts do módulo. Em standalone o serviço sobe na porta 3012 conforme DEFAULT_MODULE_PORTS. Um detalhe importante do modo standalone: o authMiddleware resolve o ApiKeyAuthService pelo container TypeDI local e, se ele não estiver registrado naquele processo, responde 401 com API_KEY_UNAVAILABLE em vez de falhar de forma obscura.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Chave completa | prefixo.segredo. O único momento em que ela existe inteira é a resposta da criação ou da rotação. |
| Prefixo | 8 caracteres em alfabeto sem letras ambíguas. Gravado em claro, é o índice de busca e o identificador em log. |
| Segredo | 32 caracteres alfanuméricos. Guardado apenas como hash Argon2id. |
| Allowed resources | Lista de padrões MÉTODO /caminho. Vazia significa acesso total. |
| Rotação | Novo segredo com o mesmo prefixo; o hash anterior sobrevive até gracePeriodEnds. |
| Período de graça | Janela em que a chave antiga e a nova autenticam. De 1 a 168 horas, padrão 24. |
| Revogação | status vira REVOKED, revokedAt é preenchido e as rotações pendentes são apagadas. |
| Exclusão | Lógica: deletedAt preenchido, a linha permanece para auditoria. |
| Balde de uso | Registro por chave e por hora com contagem de requisições, de erros e detalhamento por endpoint. |
| Admin de chaves | Quem tem API_KEYS_DELETE no token. Enxerga e opera chaves de outros usuários da mesma organização. |
Modelo de dados — schema apikeys no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
ApiKey | apikeys.api_keys | A chave | prefix, secretHash, status, allowedResources, rateLimitMax, rateLimitWindowMs, expiresAt, lastUsedAt, revokedAt, deletedAt. Únicos (organizationId, prefix) e (organizationId, userId, name) |
ApiKeyRotation | apikeys.api_key_rotations | Hash anterior durante o período de graça | apiKeyId, previousHash, gracePeriodEnds. Cascata ao apagar a chave |
ApiKeyUsageRecord | apikeys.api_key_usage_records | Uso agregado por hora | apiKeyId, periodStart, periodEnd, requestCount, errorCount, endpointBreakdown (JSONB). Único (apiKeyId, periodStart) |
Ciclo de vida da chave
stateDiagram-v2
[*] --> ACTIVE: POST /api-keys<br/>chave completa exibida uma vez
ACTIVE --> EmRotacao: POST /:keyId/rotate<br/>novo segredo, mesmo prefixo
EmRotacao --> ACTIVE: passa gracePeriodEnds<br/>só a chave nova autentica
ACTIVE --> ExpiradaDeFato: chega em expiresAt
ACTIVE --> REVOKED: POST /:keyId/revoke
EmRotacao --> REVOKED: POST /:keyId/revoke<br/>rotações pendentes apagadas
ExpiradaDeFato --> REVOKED: POST /:keyId/revoke
ACTIVE --> Excluida: DELETE /:keyId
REVOKED --> Excluida: DELETE /:keyId
Excluida --> [*]
note right of EmRotacao
Não é um status no banco.
A chave continua ACTIVE e
o hash antigo vive em
ApiKeyRotation até a janela fechar.
end note
note right of ExpiradaDeFato
Também não é status no banco.
A autenticação recusa por
expiresAt, mas o status
permanece ACTIVE (ver §15).
end noteEnumerações
| Enum | Valores | Observação |
|---|---|---|
ApiKeyStatus | ACTIVE · REVOKED · EXPIRED | EXPIRED existe no enum do Prisma mas nenhum código o atribui — ver §15 |
Anatomia da chave
Atenção. O desenho acima usa marcadores no lugar dos caracteres reais, de propósito. Nunca cole chave real — nem trecho dela além do prefixo — em documentação, ticket ou mensagem.
Subcontas: revenda como SaaS
Uma organização que revende um building block para os próprios clientes cadastra cada cliente como subconta dentro dela — não como organização da plataforma. O tenant continua sendo o único cliente da plataforma; a subconta é o recorte de dados e de credenciais do cliente dele.
| Termo | Significa |
|---|---|
| Subconta | Cliente do tenant. name, externalRef (o id do cliente no sistema do tenant, único na organização), status (ACTIVE ou SUSPENDED), monthlyQuota (teto mensal de uso; null = sem teto) e metadata livre. Não se apaga: suspender é o desligar, e o histórico fica para faturar e auditar. |
| Chave de subconta | Chave com subaccountId. Toda requisição feita com ela age dentro da subconta: vê e cria só os dados dela, nos BBs que conhecem subconta (biometrics, webhooks-engine). Não gere chaves nem subcontas. |
environment | Rótulo live ou test gravado na chave e exposto no contexto da requisição, para o BB separar sandbox de produção. |
X-Subaccount-Id | Header com que a chave (ou o usuário) da organização age como uma das suas subcontas — o backoffice do tenant opera o cliente sem ter a chave dele. |
Regras que a autenticação aplica em todos os serviços:
| Credencial | X-Subaccount-Id | Resultado |
|---|---|---|
| Chave ou usuário da organização | ausente | alcance da organização inteira |
| Chave ou usuário da organização | subconta da própria organização | age como a subconta |
| Chave ou usuário da organização | subconta de outra organização | 403 SUBACCOUNT_FORBIDDEN |
| Chave de subconta | ausente ou a mesma | age como a subconta da chave |
| Chave de subconta | outra subconta | 403 SUBACCOUNT_SCOPE_MISMATCH |
| Chave de subconta suspensa | — | 403 SUBACCOUNT_SUSPENDED, com o motivo |
| Chave de subconta em BB que não trabalha com subconta | — | 403 SUBACCOUNT_SCOPE_UNSUPPORTED |
A suspensão vale na requisição seguinte ao PATCH: o cache da subconta é invalidado na hora. O
serviço guarda o estado da subconta no Redis por no máximo 60 segundos.
flowchart LR T[Backoffice do tenant<br/>chave da organização] -->|POST /subaccounts| S[(Subconta<br/>cliente do tenant)] T -->|POST /subaccounts/:id/api-keys| K[Chave da subconta] K -->|X-API-Key| B[BB que conhece subconta<br/>biometrics, webhooks-engine] T -->|X-Subaccount-Id| B T -->|PATCH status=SUSPENDED| S
Como um padrão de allowedResources é avaliado
| Padrão | Casa com | Não casa com |
|---|---|---|
GET /commerce/api/v1/orders | Exatamente essa rota | /commerce/api/v1/orders/123 |
GET /commerce/api/v1/orders/* | Um segmento a mais: /orders/123 | /orders/123/items |
GET /commerce/api/v1/orders/** | Qualquer coisa a partir dali | Outro método |
* /customers/api/v1/persons/** | Qualquer método sob esse caminho | Outro caminho |
* casa um segmento (vira [^/]+); ** no fim casa o resto do caminho. A comparação ignora maiúsculas e minúsculas. Padrão malformado vira uma expressão que nunca casa, então erro de digitação nega acesso em vez de liberar.
Referência da API
Prefixo: /api-keys. Repare que o segmento api-keys aparece duas vezes na rota — uma do basePath('/api-keys') do módulo e outra do caminho em que o router é montado. As rotas abaixo estão completas e literais.
Todas as 14 rotas exigem authMiddleware e requireOrganization: token sem organizationId recebe 403. Chave de subconta recebe 403 SUBACCOUNT_SCOPE_FORBIDDEN em todas elas — gerir chaves e subcontas é da organização.
Chaves de API — /api-keys/api/v1/api-keys
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api-keys/api/v1/api-keys | Cria a chave e devolve o valor completo, uma única vez | API_KEYS_CREATE |
GET | /api-keys/api/v1/api-keys | Lista as chaves, paginado | API_KEYS_READ |
GET | /api-keys/api/v1/api-keys/:keyId | Busca uma chave (sem o segredo) | API_KEYS_READ |
PATCH | /api-keys/api/v1/api-keys/:keyId | Atualiza nome, escopos, limites e expiração | API_KEYS_UPDATE |
POST | /api-keys/api/v1/api-keys/:keyId/rotate | Gera novo segredo com período de graça | API_KEYS_ROTATE |
POST | /api-keys/api/v1/api-keys/:keyId/revoke | Revoga a chave imediatamente | API_KEYS_DELETE |
DELETE | /api-keys/api/v1/api-keys/:keyId | Exclusão lógica | API_KEYS_DELETE |
GET | /api-keys/api/v1/api-keys/:keyId/usage | Estatísticas de uso por período | API_KEYS_READ_USAGE |
Na criação, subaccountId (opcional) prende a chave a uma subconta da organização e
environment (live ou test, padrão live) rotula o ambiente; os dois voltam nos atributos.
Sem subaccountId no corpo, vale o X-Subaccount-Id. A listagem aceita
filter[subaccountId] e filter[environment].
Subcontas — /api-keys/api/v1/subaccounts
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api-keys/api/v1/subaccounts | Cria a subconta (name, externalRef?, monthlyQuota?, metadata?); externalRef repetido na organização é 409 | SUBACCOUNTS_CREATE |
GET | /api-keys/api/v1/subaccounts | Lista, paginado; filter[status], filter[externalRef] e search (nome ou externalRef) | SUBACCOUNTS_READ |
GET | /api-keys/api/v1/subaccounts/:id | Busca uma subconta | SUBACCOUNTS_READ |
PATCH | /api-keys/api/v1/subaccounts/:id | Nome, externalRef, monthlyQuota, metadata, status e suspendedReason | SUBACCOUNTS_UPDATE |
POST | /api-keys/api/v1/subaccounts/:id/api-keys | Emite chave da subconta (mesmo corpo da criação de chave) | API_KEYS_CREATE |
GET | /api-keys/api/v1/subaccounts/:id/api-keys | Chaves da subconta, paginado | API_KEYS_READ |
Suspender grava suspendedAt; reativar (status: "ACTIVE") limpa suspendedAt e
suspendedReason. Emitir chave para subconta suspensa é 403 SUBACCOUNT_SUSPENDED.
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /api-keys/health | Sonda de disponibilidade do serviço |
Quem enxerga o quê
Além da permissão, há uma regra de propriedade. Quem tem API_KEYS_DELETE é tratado como administrador de chaves e opera qualquer chave da organização; quem não tem só enxerga e opera as próprias.
flowchart TD
R["Requisição em /:keyId"] --> P["requirePermission"]
P --> O["requireOrganization"]
O --> G["getKeyWithOwnership(keyId, organizationId, userId, isAdmin)"]
G --> Q1{"A chave existe<br/>nesta organização?"}
Q1 -->|não| E404["404 NOT_FOUND"]
Q1 -->|sim| Q2{"O token tem<br/>API_KEYS_DELETE?"}
Q2 -->|sim| OK["Segue"]
Q2 -->|não| Q3{"A chave é<br/>do próprio usuário?"}
Q3 -->|sim| OK
Q3 -->|não| E403["403 FORBIDDEN"]O mesmo vale na listagem: sem API_KEYS_DELETE, o filtro filter[userId] é ignorado e a resposta traz apenas as chaves do próprio usuário.
POST /api-keys/api/v1/api-keys
Aceita o corpo direto ou embrulhado em data.attributes (JSON:API).
Request
{
"name": "Marketplace Alfa — leitura",
"description": "Consulta de pedidos e catálogo",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-01-31T23:59:59.000Z"
}{
"name": "Marketplace Alfa — leitura",
"description": "Consulta de pedidos e catálogo",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-01-31T23:59:59.000Z"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único por (organização, usuário). Colisão devolve 409 |
description | string (≤ 500) | Não | Texto livre |
allowedResources | string[] | Não | Padrões MÉTODO /caminho. Omitido ou vazio libera tudo |
rateLimitMax | int (1–1.000.000) | Não | Sem valor, não há limite por chave |
rateLimitWindowMs | int (1–86.400.000) | Não | Janela do limite. Precisa vir junto com rateLimitMax |
expiresAt | date | Não | Sem valor, a chave não expira |
Resposta 201
{
"data": {
"type": "api-keys",
"id": "c7d2a1b0-0000-0000-0000-000000000001",
"links": { "self": "/api/v1/api-keys/c7d2a1b0-0000-0000-0000-000000000001" },
"attributes": {
"name": "Marketplace Alfa — leitura",
"description": "Consulta de pedidos e catálogo",
"prefix": "e8k3mqz7",
"status": "ACTIVE",
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"expiresAt": "2027-01-31T23:59:59.000Z",
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-08-17T12:00:00.000Z",
"updatedAt": "2026-08-17T12:00:00.000Z"
}
},
"key": "<chave completa — prefixo.segredo — exibida apenas aqui>"
}{
"data": {
"type": "api-keys",
"id": "c7d2a1b0-0000-0000-0000-000000000001",
"links": { "self": "/api/v1/api-keys/c7d2a1b0-0000-0000-0000-000000000001" },
"attributes": {
"name": "Marketplace Alfa — leitura",
"description": "Consulta de pedidos e catálogo",
"prefix": "e8k3mqz7",
"status": "ACTIVE",
"rateLimitMax": 600,
"rateLimitWindowMs": 60000,
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"GET /products/api/v1/products/**"
],
"expiresAt": "2027-01-31T23:59:59.000Z",
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-08-17T12:00:00.000Z",
"updatedAt": "2026-08-17T12:00:00.000Z"
}
},
"key": "<chave completa — prefixo.segredo — exibida apenas aqui>"
}Atenção. O campo key só aparece nesta resposta. Nenhuma outra rota o devolve, e não existe forma de recuperá-lo: o banco só guarda o hash. Perdeu, rotacione.
| Erro | Quando |
|---|---|
400 VALIDATION | Padrão de allowedResources fora do formato MÉTODO /caminho, limite fora da faixa |
403 | Token sem organizationId ou sem API_KEYS_CREATE |
409 CONFLICT | Já existe chave com esse nome para o mesmo usuário na organização |
POST /api-keys/api/v1/api-keys/:keyId/rotate
Request (corpo opcional)
{ "gracePeriodHours": 72 }{ "gracePeriodHours": 72 }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
gracePeriodHours | int (1–168) | Não | Padrão 24. Quanto tempo a chave antiga continua aceita |
Resposta 200
{
"data": { "type": "api-keys", "id": "...", "attributes": { "prefix": "e8k3mqz7", "status": "ACTIVE" } },
"key": "<chave nova — mesmo prefixo, segredo novo — exibida apenas aqui>",
"gracePeriodEnds": "2026-08-20T12:00:00.000Z"
}{
"data": { "type": "api-keys", "id": "...", "attributes": { "prefix": "e8k3mqz7", "status": "ACTIVE" } },
"key": "<chave nova — mesmo prefixo, segredo novo — exibida apenas aqui>",
"gracePeriodEnds": "2026-08-20T12:00:00.000Z"
}O prefixo não muda. Chave revogada não pode ser rotacionada e devolve 400.
GET /api-keys/api/v1/api-keys
| Parâmetro de query | Descrição |
|---|---|
page[number], page[size] | Paginação padrão da plataforma; pageSize máximo 100 |
filter[status] | ACTIVE, REVOKED ou EXPIRED |
filter[userId] | Ignorado para quem não tem API_KEYS_DELETE |
GET /api-keys/api/v1/api-keys/:keyId/usage
| Parâmetro de query | Tipo | Obrigatório | Descrição |
|---|---|---|---|
startDate | date | Sim | Início do período. Ausente devolve 400 |
endDate | date | Sim | Fim do período. Ausente devolve 400 |
Resposta 200
{
"data": {
"totalRequests": 184203,
"totalErrors": 0,
"periodData": [
{
"periodStart": "2026-07-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:59:59.999Z",
"requestCount": 412,
"errorCount": 0,
"endpointBreakdown": { "GET /commerce/api/v1/orders": 400, "GET /products/api/v1/products": 12 }
}
]
}
}{
"data": {
"totalRequests": 184203,
"totalErrors": 0,
"periodData": [
{
"periodStart": "2026-07-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:59:59.999Z",
"requestCount": 412,
"errorCount": 0,
"endpointBreakdown": { "GET /commerce/api/v1/orders": 400, "GET /products/api/v1/products": 12 }
}
]
}
}Atenção. totalErrors e errorCount são sempre 0 hoje — a contabilização registra toda requisição autenticada como sucesso. Ver §15.
Autenticando com a chave
A chave não é usada nas rotas acima; ela é usada em qualquer building block. Duas formas equivalentes:
# Header dedicado
curl -s https://commerce.bb.stg.catalisa.app/commerce/api/v1/orders \
-H "X-API-Key: $API_KEY"
# Ou pelo Authorization, com o esquema ApiKey
curl -s https://commerce.bb.stg.catalisa.app/commerce/api/v1/orders \
-H "Authorization: ApiKey $API_KEY"# Header dedicado
curl -s https://commerce.bb.stg.catalisa.app/commerce/api/v1/orders \
-H "X-API-Key: $API_KEY"
# Ou pelo Authorization, com o esquema ApiKey
curl -s https://commerce.bb.stg.catalisa.app/commerce/api/v1/orders \
-H "Authorization: ApiKey $API_KEY"O Bearer continua reservado ao JWT do IAM. Se houver Authorization: Bearer ..., o middleware nem procura chave.
| Erro na autenticação | Significa |
|---|---|
401 "Invalid API key format" | A chave não está no formato prefixo.segredo |
401 "Invalid API key" | Prefixo inexistente ou segredo que não bate com nenhum hash válido |
401 "API key is revoked" | status diferente de ACTIVE |
401 "API key has expired" | expiresAt no passado |
403 "API key does not have permission for this resource" | O método e o caminho não casam com nenhum allowedResources |
403 "Rate limit exceeded. Retry after N seconds" | Estourou rateLimitMax na janela da chave |
Início rápido
Do zero a uma chamada autenticada por chave. Cada passo com a resposta esperada.
Passo 1 — obter um token do IAM.
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)Esperado: um JWT. O token precisa carregar API_KEYS_CREATE.
Passo 2 — criar a chave e guardar o valor.
RESP=$(curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Teste de integração",
"allowedResources": ["GET /customers/api/v1/persons/**"],
"rateLimitMax": 60,
"rateLimitWindowMs": 60000
}')
API_KEY=$(echo "$RESP" | jq -r .key)
KEY_ID=$(echo "$RESP" | jq -r .data.id)
echo "$API_KEY" | cut -d. -f1 # só o prefixo, seguro de mostrarRESP=$(curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Teste de integração",
"allowedResources": ["GET /customers/api/v1/persons/**"],
"rateLimitMax": 60,
"rateLimitWindowMs": 60000
}')
API_KEY=$(echo "$RESP" | jq -r .key)
KEY_ID=$(echo "$RESP" | jq -r .data.id)
echo "$API_KEY" | cut -d. -f1 # só o prefixo, seguro de mostrarEsperado: 201. O jq -r .key traz a chave completa — este é o único momento. O cut mostra apenas o prefixo, que é o que se pode colar em ticket.
Passo 3 — usar a chave no que ela pode.
curl -s -o /dev/null -w "%{http_code}\n" \
https://customers.bb.stg.catalisa.app/customers/api/v1/persons \
-H "X-API-Key: $API_KEY"curl -s -o /dev/null -w "%{http_code}\n" \
https://customers.bb.stg.catalisa.app/customers/api/v1/persons \
-H "X-API-Key: $API_KEY"Esperado: 200. Repare que a chamada foi para o Customers, não para o API Keys — é assim que a chave se usa.
Passo 4 — confirmar que o escopo barra o resto.
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://customers.bb.stg.catalisa.app/customers/api/v1/persons \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" -d '{}'curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://customers.bb.stg.catalisa.app/customers/api/v1/persons \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" -d '{}'Esperado: 403. O padrão liberado era só GET, e o 403 chega antes da regra de negócio.
Passo 5 — ver o consumo.
HOJE=$(date -u +%Y-%m-%d)
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=$HOJE&endDate=$HOJE" \
-H "Authorization: Bearer $TOKEN" | jq '.data.totalRequests'HOJE=$(date -u +%Y-%m-%d)
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=$HOJE&endDate=$HOJE" \
-H "Authorization: Bearer $TOKEN" | jq '.data.totalRequests'Esperado: a contagem das chamadas dos passos 3 e 4.
Passo 6 — revogar.
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $TOKEN" | jqEsperado: { "success": true }. Chamadas seguintes com a chave passam a receber 401 assim que o cache da chave expirar — ver a armadilha na §11.
Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção nem cole chave real em documentação. Os comandos acima não foram executados contra staging na escrita deste documento.
Receitas
Entregar uma chave a um parceiro com o mínimo de privilégio
Objetivo. Emitir credencial que só faz o que o contrato prevê.
curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "ERP do Cliente Beta",
"description": "Leitura de pedidos e escrita de status de entrega",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"PATCH /commerce/api/v1/orders/*/shipping"
],
"rateLimitMax": 300,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-06-30T23:59:59.000Z"
}'curl -s -X POST https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "ERP do Cliente Beta",
"description": "Leitura de pedidos e escrita de status de entrega",
"allowedResources": [
"GET /commerce/api/v1/orders/**",
"PATCH /commerce/api/v1/orders/*/shipping"
],
"rateLimitMax": 300,
"rateLimitWindowMs": 60000,
"expiresAt": "2027-06-30T23:59:59.000Z"
}'Armadilhas.
- Omitir
allowedResourceslibera a API inteira. Lista vazia ou ausente devolvetruena verificação de recurso. É o erro mais caro possível neste módulo. - O caminho no padrão é a rota real, com o prefixo do building block —
/commerce/api/v1/orders, não/orders. É opathnameda requisição que é comparado. *casa um segmento e**casa o resto.GET /orders/*não cobre/orders/123/items.rateLimitMaxsemrateLimitWindowMsnão ativa limite nenhum: o código exige os dois.- Entregue a chave por canal seguro. Ela aparece uma vez e não há como reexibir.
Revender um BB: uma subconta por cliente
# 1. cliente novo no backoffice do tenant
curl -X POST "$BASE/api-keys/api/v1/subaccounts" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Padaria do João","externalRef":"cliente-123","monthlyQuota":500}'
# 2. chave de produção do cliente, só para o biometrics
curl -X POST "$BASE/api-keys/api/v1/subaccounts/$SUB_ID/api-keys" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"producao","environment":"live","allowedResources":["* /biometrics/**"]}'
# 3. inadimplência: suspende; as chaves do cliente passam a receber 403 SUBACCOUNT_SUSPENDED
curl -X PATCH "$BASE/api-keys/api/v1/subaccounts/$SUB_ID" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' -d '{"status":"SUSPENDED","suspendedReason":"fatura vencida"}'# 1. cliente novo no backoffice do tenant
curl -X POST "$BASE/api-keys/api/v1/subaccounts" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Padaria do João","externalRef":"cliente-123","monthlyQuota":500}'
# 2. chave de produção do cliente, só para o biometrics
curl -X POST "$BASE/api-keys/api/v1/subaccounts/$SUB_ID/api-keys" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"producao","environment":"live","allowedResources":["* /biometrics/**"]}'
# 3. inadimplência: suspende; as chaves do cliente passam a receber 403 SUBACCOUNT_SUSPENDED
curl -X PATCH "$BASE/api-keys/api/v1/subaccounts/$SUB_ID" -H "X-API-Key: $ORG_KEY" \
-H 'Content-Type: application/json' -d '{"status":"SUSPENDED","suspendedReason":"fatura vencida"}'Para operar o cliente pelo backoffice sem a chave dele, a chave da organização manda
X-Subaccount-Id: $SUB_ID — por exemplo, para ler o uso dele em GET /biometrics/api/v1/usage.
Rotacionar sem derrubar o parceiro
Objetivo. Trocar o segredo com janela de convivência.
# 1. Rotacionar com 72h de graça
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/rotate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "gracePeriodHours": 72 }' | jq '{ key, gracePeriodEnds }'# 1. Rotacionar com 72h de graça
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/rotate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "gracePeriodHours": 72 }' | jq '{ key, gracePeriodEnds }'# 2. Antes de a janela fechar, confirmar que o tráfego migrou
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=$ONTEM&endDate=$HOJE" \
-H "Authorization: Bearer $TOKEN" | jq '.data.totalRequests'# 2. Antes de a janela fechar, confirmar que o tráfego migrou
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/usage?startDate=$ONTEM&endDate=$HOJE" \
-H "Authorization: Bearer $TOKEN" | jq '.data.totalRequests'Armadilhas.
- O prefixo não muda. Quem espera que a chave nova pareça diferente vai se confundir; só o trecho depois do ponto muda.
- O período de graça máximo é 168 horas (7 dias). Migração maior que isso exige criar uma chave nova em vez de rotacionar.
- O uso agregado não separa a chave antiga da nova: as duas contam no mesmo
apiKeyId. Não dá para responder "o parceiro já migrou?" pelo relatório de uso — combine a confirmação com ele. - Rotacionar uma chave revogada devolve
400.
Revogar uma chave comprometida
Objetivo. Cortar o acesso imediatamente.
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $TOKEN"curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $TOKEN"O status vira REVOKED, revokedAt é preenchido e as rotações pendentes são apagadas — a chave antiga do período de graça morre junto.
Atenção. A verificação usa cache no Redis com TTL de API_KEY_CACHE_TTL_SECONDS (300 segundos por padrão). Uma chave verificada momentos antes da revogação pode continuar respondendo até o cache daquele prefixo expirar. Em incidente, trate os 5 minutos seguintes como janela em que a chave ainda pode funcionar, e escale conforme o seu procedimento de resposta.
Descobrir por que uma chave está recebendo 403
# O que a chave permite, de fato
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes | {allowedResources, rateLimitMax, rateLimitWindowMs, status, expiresAt}'# O que a chave permite, de fato
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes | {allowedResources, rateLimitMax, rateLimitWindowMs, status, expiresAt}'Ordem de diagnóstico:
- O
statuséACTIVE? Se não, é401, não403. - O caminho que o parceiro chama é exatamente o que está no padrão, com o prefixo do building block?
- O método bate?
GET /rotanão cobreHEADnemPOST. - O curinga é o certo? Um
*a menos e/orders/123/itemsnão casa. - Se tudo bate, é limite de vazão — a mensagem do
403dizRetry after N seconds.
Auditar quais chaves ainda estão em uso
curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys?page%5Bsize%5D=100&filter%5Bstatus%5D=ACTIVE" \
-H "Authorization: Bearer $TOKEN" \
| jq '.data[] | { name: .attributes.name, prefix: .attributes.prefix, lastUsedAt: .attributes.lastUsedAt }'curl -s "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys?page%5Bsize%5D=100&filter%5Bstatus%5D=ACTIVE" \
-H "Authorization: Bearer $TOKEN" \
| jq '.data[] | { name: .attributes.name, prefix: .attributes.prefix, lastUsedAt: .attributes.lastUsedAt }'Chave ACTIVE com lastUsedAt nulo ou muito antigo é candidata a revogação. Precisa de API_KEYS_DELETE para enxergar as chaves de todos os usuários da organização; sem isso, a lista traz só as suas.
Armadilha. lastUsedAt é atualizado em modo fire and forget a cada autenticação. Sob carga, a gravação pode falhar em silêncio, então trate o campo como indicador, não como registro contábil.
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token que autoriza criar e gerir chaves, e define o organizationId que a chave herda | Sim |
| Todos os demais | Aceitam autenticação por chave pelo authMiddleware compartilhado, sem código próprio | — |
| Audit Trail | Consome apikeys.key.created, apikeys.key.updated, apikeys.key.rotated e apikeys.key.revoked | Não |
| Webhooks Engine | Pode entregar esses eventos a sistemas externos | Não |
| SSO | Caminho paralelo, para pessoa em vez de máquina | Não |
Qual dos três eu uso?
O IAM, o SSO e o API Keys respondem juntos à mesma pergunta. A régua é quem está do outro lado e por quanto tempo a credencial precisa durar.
flowchart TD
Q1{"Quem está autenticando?"}
Q1 -->|"Uma pessoa"| Q2{"Com a conta corporativa dela<br/>(Google, Microsoft, ...)?"}
Q1 -->|"Um sistema"| Q3{"A integração precisa de<br/>escopo dinâmico e token<br/>de vida curta?"}
Q2 -->|sim| SSO["**SSO**<br/>asserção federada"]
Q2 -->|não| IAM["**IAM**<br/>e-mail e senha → access token de 1h"]
Q3 -->|sim| IAM2["**IAM**<br/>client_credentials → token de 1h"]
Q3 -->|não| AK["**API Keys**<br/>chave estática, escopo por rota,<br/>rotação com período de graça"]| Dimensão | IAM (JWT) | API Keys |
|---|---|---|
| Vida da credencial | 1 hora, renovável | Longa, até expiresAt ou revogação |
| Modelo de autorização | RBAC — permissões no token | Padrões MÉTODO /caminho na chave |
| Onde a autorização é verificada | requirePermission contra token.permissions | requirePermission contra allowedResources |
| Revogação | Só na renovação do token | Imediata, com a ressalva do cache da §11 |
| Limite de vazão | Global e nas rotas de autenticação | Por chave, configurável |
| Medição de uso | Não | Sim, por hora e por endpoint |
| Melhor para | Integração viva, com escopo que muda | Integração simples, com credencial fixa |
Onde o API Keys entra na cadeia
flowchart LR Op["Operador com API_KEYS_CREATE"] -->|"POST /api-keys"| AK["API Keys"] AK -->|"chave completa, uma vez"| Pa["Sistema do parceiro"] Pa -->|"X-API-Key"| BB["Customers · Commerce · Billing · ..."] BB --> MW["authMiddleware<br/>chama ApiKeyAuthService"] MW -->|"organizationId da chave"| D["Dados apenas desta organização"] MW -.->|"trackUsage"| AK AK -.->|"apikeys.key.*"| AT["Audit Trail"]
O argumento comercial está na seta do meio: a chave é emitida uma vez, num lugar só, e todos os building blocks passam a aceitá-la. Não há integração a fazer em cada serviço, e não há um segundo modelo de credencial para auditar. Adicionar um building block novo ao catálogo não adiciona trabalho nenhum aqui.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
API_KEY_CACHE_TTL_SECONDS | Vida no Redis da chave já verificada. É o que define a janela em que uma revogação ainda não surtiu efeito | Não | 300 |
DATABASE_URL | PostgreSQL, schema apikeys | Sim | — |
REDIS_URL | Redis, usado no cache de verificação e no limite de vazão | Sim | — |
JWT_SECRET | Segredo HS256 do IAM, para as rotas administrativas | Sim | — |
PORT | Porta em standalone | Não | 3012 conforme DEFAULT_MODULE_PORTS |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
Atenção. Existem outras seis variáveis API_KEY_* declaradas no schema de ambiente — API_KEY_DEFAULT_RATE_LIMIT_MAX, API_KEY_DEFAULT_RATE_LIMIT_WINDOW_MS, API_KEY_PREFIX_LENGTH, API_KEY_SECRET_LENGTH, API_KEY_MIN_GRACE_PERIOD_HOURS e API_KEY_MAX_GRACE_PERIOD_HOURS. Nenhuma delas é lida por código algum hoje. Os valores efetivos são as constantes descritas na §8 e na §15. Mudá-las não muda comportamento.
Dependências de infraestrutura
| Dependência | Para quê | Se cair |
|---|---|---|
PostgreSQL (schema apikeys) | Chaves, rotações e uso | Nenhuma chave autentica em nenhum building block |
| Redis | Cache de verificação e limite de vazão por chave | Toda verificação vira Argon2id frio, o que é lento; sob carga vira gargalo |
| IAM | Token das rotas administrativas | Não dá para criar nem gerir chaves; as existentes continuam autenticando |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Tamanho do prefixo | 8 caracteres, alfabeto de 31 símbolos | Constante em key-generator.ts |
| Tamanho do segredo | 32 caracteres base62 | Constante em key-generator.ts |
| Nome da chave | 1 a 100 caracteres, único por (organização, usuário) | Zod + único no banco |
| Descrição | Até 500 caracteres | Zod |
rateLimitMax | 1 a 1.000.000 | Zod |
rateLimitWindowMs | 1 a 86.400.000 (24 horas) | Zod |
| Período de graça | 1 a 168 horas, padrão 24 | Zod |
| Paginação | pageSize máximo 100 | Zod |
| Corpo da requisição | 1 MB | applyCommonMiddleware |
| Tentativas de gerar prefixo único | 10, depois falha com erro de validação | generateUniquePrefix |
Catálogo de erros
| Código | Significa | O que fazer |
|---|---|---|
VALIDATION (400) | Padrão de recurso malformado, limite fora da faixa, período de graça inválido, ou tentativa de atualizar ou rotacionar chave revogada | Confira o formato em §9 |
UNAUTHORIZED (401) | Chave em formato inválido, inexistente, revogada ou expirada | Confira o status e o expiresAt da chave |
FORBIDDEN (403) | Sem organizationId, sem permissão, chave de outro usuário, recurso fora do escopo, ou limite de vazão estourado | Ver o diagnóstico em §11 |
NOT_FOUND (404) | Chave inexistente nesta organização | Liste antes de operar por ID |
CONFLICT (409) | Nome de chave repetido para o mesmo usuário | Escolha outro nome |
INTERNAL (500) | Falha ao gerar hash, gravar ou publicar evento | Verifique Postgres e Redis |
Observabilidade
| Sinal | Onde | Para quê |
|---|---|---|
apikeys.key.created · key.updated · key.rotated · key.revoked | Eventos publicados, com apiKeyId, prefix e name | Trilha de auditoria do ciclo de vida |
PERMISSION_DENIED no log de segurança | Emitido pelo requirePermission com o prefixo da chave | Investigar chave tentando o que não pode, sem precisar da chave |
ApiKeyUsageRecord | Balde horário por chave, com detalhamento por endpoint | Consumo e dimensionamento |
lastUsedAt | Campo da chave | Encontrar credencial ociosa |
cleanupOldRecords(retentionDays) | Método do ApiKeyUsageService | Expurgo de registros antigos. Não há agendador chamando isso — ver §15 |
Segurança e compliance
Isolamento entre tenants — especificamente, no código.
O organizationId nunca é informado pelo cliente. Na criação, ele vem do token do IAM. Na autenticação por chave, ele vem do registro da própria chave e é injetado no contexto da requisição.
flowchart TD
A["Requisição com X-API-Key"] --> B["ApiKeyAuthService.authenticate"]
B --> C["ApiKeyContext<br/>organizationId · userId · prefix · allowedResources"]
C --> D["authMiddleware normaliza para c.set('user')<br/>com o organizationId da chave"]
D --> E["requireOrganization do building block"]
E --> F["Serviço recebe organizationId<br/>e filtra toda consulta por ele"]
F --> G["Dados apenas desta organização"]
H["Corpo da requisição"] -.->|"nunca é fonte<br/>de organizationId"| FNas rotas administrativas do módulo, o filtro é explícito em todo repositório: findById(id, organizationId), findMany({ organizationId }), nameExists(name, organizationId, userId). Uma chave de outra organização responde 404, não 403 — o módulo não confirma sequer a existência do recurso.
Há uma segunda camada, de propriedade dentro da organização: sem API_KEYS_DELETE, o usuário só enxerga e opera as chaves que ele mesmo criou, conforme o fluxo desenhado na §9.
Dados sensíveis e como são tratados
| Dado | Tratamento |
|---|---|
| Segredo da chave | Hash Argon2id — argon2id, 64 MB de memória, 3 passes. O texto puro existe apenas na resposta de criação e de rotação, e nunca é gravado |
| Prefixo | Gravado em claro de propósito: é o índice de busca e o identificador que aparece em log, para que investigar não exija a chave |
| Hash anterior, durante a rotação | Gravado em api_key_rotations e apagado quando a chave é revogada ou excluída |
| Chave em cache | O Redis guarda o hash, status, escopos e limites — nunca a chave. Um dump do cache não devolve credencial |
| Chave em log | O log de segurança registra apiKeyPrefix, não a chave |
| Registros de uso | Contêm método, caminho e contagens. Não contêm corpo de requisição nem parâmetro |
Autenticação e permissões exigidas
| Permissão | Concede |
|---|---|
API_KEYS_CREATE | Criar chave |
API_KEYS_READ | Listar e ler chaves (nunca o segredo) |
API_KEYS_UPDATE | Alterar nome, escopos, limites e expiração |
API_KEYS_ROTATE | Rotacionar |
API_KEYS_DELETE | Revogar e excluir. Também confere visão administrativa sobre as chaves dos outros usuários da organização |
API_KEYS_READ_USAGE | Consultar estatísticas de uso |
A autorização por chave é um modelo diferente da autorização por JWT. Isto merece atenção de quem faz revisão de segurança: quando a requisição chega com chave, o requirePermission não avalia permissões de RBAC — ele avalia método e caminho contra allowedResources. Uma chave com escopo aberto passa por qualquer requirePermission de qualquer building block. Consequência prática: allowedResources é o único controle de autorização de uma chave. Preencher esse campo não é boa prática opcional, é o controle.
Atenção. Lista de allowedResources vazia ou ausente significa acesso total a tudo o que a plataforma expõe para aquela organização. Trate a criação de chave sem escopo como exceção que exige justificativa, e prefira negar por padrão nos seus procedimentos internos.
Enquadramento regulatório
- LGPD. O módulo em si guarda pouco dado pessoal:
userIdde quem criou a chave e os caminhos chamados. O ponto de atenção é indireto — uma chave escopada de forma ampla dá acesso a dados pessoais que vivem em outros building blocks. O controle de minimização, aqui, é o escopo. - Trilha de auditoria. Criação, atualização, rotação e revogação publicam evento. Exclusão é lógica (
deletedAt), então a linha permanece disponível para auditoria posterior. - Segregação de funções. A permissão que revoga é a mesma que confere visão sobre as chaves de terceiros. Se a sua política exige separar quem revoga de quem audita, isso precisa ser tratado no desenho de papéis, porque o módulo não distingue os dois.
Boas práticas de operação
- Sempre preencha
allowedResourceseexpiresAt. Chave eterna sem escopo é o padrão de risco que este módulo existe para eliminar. - Entregue a chave por canal seguro e oriente o parceiro a guardá-la em cofre de segredos, não em variável de ambiente versionada.
- Em suspeita de vazamento, revogue — não rotacione. Rotação mantém a chave antiga viva pelo período de graça; revogação apaga as rotações pendentes.
- Guarde as suas próprias chaves com SOPS, como os demais segredos da plataforma — ver SECRETS-SOPS-REFERENCE.md.
- Monitore
lastUsedAte revogue o que está ocioso. Credencial esquecida é a que ninguém percebe sendo usada.
Limitações conhecidas
O status EXPIRED existe no banco mas nunca é atribuído
O enum ApiKeyStatus tem ACTIVE, REVOKED e EXPIRED, e o filtro filter[status]=EXPIRED aceita o valor. Mas nenhum código escreve EXPIRED: a expiração é avaliada na autenticação, comparando expiresAt com o instante atual. Na prática, uma chave vencida continua com status: "ACTIVE" na listagem, e filter[status]=EXPIRED sempre devolve lista vazia. Para encontrar chaves vencidas, filtre por expiresAt no lado de quem consulta.
Seis das sete variáveis API_KEY_* não fazem nada
API_KEY_DEFAULT_RATE_LIMIT_MAX, API_KEY_DEFAULT_RATE_LIMIT_WINDOW_MS, API_KEY_PREFIX_LENGTH, API_KEY_SECRET_LENGTH, API_KEY_MIN_GRACE_PERIOD_HOURS e API_KEY_MAX_GRACE_PERIOD_HOURS estão declaradas no schema de ambiente e não são lidas por nenhum código. Os valores efetivos são constantes: prefixo de 8, segredo de 32, período de graça de 1 a 168 horas validado no Zod. Só API_KEY_CACHE_TTL_SECONDS funciona de verdade.
Não há limite de vazão padrão
Chave criada sem rateLimitMax e rateLimitWindowMs não tem limite algum por chave — a verificação só ocorre quando os dois campos estão preenchidos. As variáveis de padrão citadas acima existiriam para isso, mas não são lidas. O rate limit global do applyCommonMiddleware continua valendo, e ele não distingue chave de chave.
errorCount é sempre zero
A contabilização de uso registra toda requisição autenticada com isError: false. Erros de negócio, 4xx e 5xx posteriores à autenticação não voltam para o contador, porque a chamada acontece antes de o handler rodar. totalErrors e errorCount no relatório são, hoje, sempre 0.
O expurgo de registros de uso não roda sozinho
ApiKeyUsageService.cleanupOldRecords(retentionDays) existe e funciona, mas nenhum agendador o chama. Sem intervenção, api_key_usage_records cresce indefinidamente — um balde por chave ativa por hora. Numa operação com 100 chaves ativas, isso é cerca de 876 mil linhas por ano.
A revogação leva até o TTL do cache para valer
A verificação usa cache no Redis por API_KEY_CACHE_TTL_SECONDS (300 segundos por padrão). Existe um método invalidateCache(prefix) no serviço de autenticação, mas ele não é chamado pelas operações de revogação, exclusão ou atualização. Consequência: revogar, alterar escopo ou reduzir limite de vazão só surte efeito completo depois que o cache daquele prefixo expira. Em incidente, conte com essa janela.
O relatório de uso não separa a chave antiga da nova durante a rotação
As duas chaves compartilham o mesmo apiKeyId, então o uso é somado. Não dá para responder "o parceiro já migrou para a chave nova?" pelos dados do módulo.
O formato não é reconhecível por scanner de segredos
A chave é prefixo.segredo, sem prefixo semântico do tipo sk_live_. O padrão não está registrado no programa de parceria de secret scanning do GitHub nem em serviço equivalente, então uma chave vazada em repositório público não dispara notificação automática. É diferença concreta contra o modelo do Stripe descrito na §4.
O ambiente é um rótulo da chave, não do formato
environment (live ou test) é gravado na chave, devolvido na listagem e exposto no contexto da requisição, mas o valor da chave (prefixo.segredo) é o mesmo formato nos dois ambientes. Um segredo de staging colado por engano em produção falha na autenticação porque o banco é outro.
Não há escopo por organização em rota cross-tenant nem chave de plataforma
Toda chave pertence a exatamente uma organização. Não existe chave de operador de plataforma capaz de atravessar organizações, como o isRoot do IAM permite para usuários.
updateKey substitui allowedResources inteiro
O PATCH não faz mesclagem: enviar allowedResources troca a lista completa. Enviar uma lista parcial reduz o escopo silenciosamente, e enviar null a esvazia — o que, pela regra da lista vazia, abre o acesso em vez de fechar. Sempre envie a lista completa e pretendida.
Perguntas frequentes
Qual é o formato da chave, exatamente?
prefixo.segredo. O prefixo tem 8 caracteres de um alfabeto sem símbolos ambíguos (abcdefghjkmnpqrstuvwxyz23456789, sem 0, o, 1, l e i); o segredo tem 32 caracteres alfanuméricos. Os dois são separados por um ponto. Não há prefixo do tipo sk_live_ nem bbk_ — se você viu isso em documentação antiga, está desatualizada.
A chave é guardada no banco?
O segredo não. O que fica gravado é o hash Argon2id dele (64 MB de memória, 3 passes), o mesmo tratamento das senhas do IAM. O prefixo, sim, fica em claro — é ele que permite localizar a chave sem testar todos os hashes do banco, e é o que aparece em log de segurança. Um dump do banco não devolve chave nenhuma.
Perdi a chave. Como recupero?
Não recupera. Ela aparece uma única vez, na resposta da criação ou da rotação. O caminho é rotacionar (POST /:keyId/rotate), o que gera um segredo novo mantendo o mesmo prefixo, ou criar uma chave nova.
Como rotaciono sem derrubar a integração?
POST /:keyId/rotate com gracePeriodHours entre 1 e 168. Durante essa janela as duas chaves autenticam, porque o hash antigo fica guardado em ApiKeyRotation até gracePeriodEnds. O prefixo não muda. Depois da janela, só a nova responde.
Revoguei uma chave e ela ainda funciona. Por quê?
Por causa do cache de verificação no Redis, com TTL de API_KEY_CACHE_TTL_SECONDS — 300 segundos por padrão. A revogação grava no banco imediatamente, mas o cache daquele prefixo só é descartado quando expira. Conte com essa janela em procedimento de incidente; está na §15.
Se eu não preencher allowedResources, o que acontece?
A chave tem acesso a tudo que a plataforma expõe para aquela organização. Lista vazia ou ausente devolve permitido na verificação de recurso. É o comportamento mais perigoso do módulo e o motivo de ele estar repetido em três seções deste documento.
Qual a diferença entre o API Keys e o IAM?
O IAM autentica identidades — pessoas e organizações — com token de vida curta e autorização por RBAC. O API Keys emite credencial de longa duração para acesso programático, com autorização por padrão de rota e rastreio de uso próprio. Integração que roda continuamente e precisa de escopo dinâmico usa client_credentials do IAM; integração simples com credencial estática usa API Keys.
Uma chave de API pode usar qualquer endpoint da plataforma?
Pode, desde que o allowedResources permita. O authMiddleware é compartilhado por todos os building blocks, então a chave funciona em Customers, Commerce, Billing e nos demais sem configuração adicional em nenhum deles. É por isso que o escopo importa tanto.
Onde coloco a chave na requisição?
Em X-API-Key: prefixo.segredo ou em Authorization: ApiKey prefixo.segredo. O esquema Bearer é reservado ao JWT do IAM: se o header começar com Bearer, o middleware nem procura chave de API.
Consigo saber quantas chamadas cada parceiro fez?
Sim, por chave. GET /:keyId/usage?startDate=...&endDate=... devolve o total do período e o detalhamento por hora e por endpoint. As duas datas são obrigatórias — sem elas a resposta é 400. A ressalva é errorCount, que hoje é sempre zero.
Um usuário comum consegue ver as chaves dos colegas?
Não. Quem não tem API_KEYS_DELETE só enxerga e opera as próprias chaves, e o filtro filter[userId] é ignorado para ele. Quem tem API_KEYS_DELETE é tratado como administrador de chaves da organização e enxerga todas — mas nunca as de outra organização.