Catalisa.Building Blocks
Catálogo/Identidade/API Keys

API Keys

Produção

Credencial programática de longa duração, com escopo por rota e rotação sem queda

14
Endpoints
4
Entidades
0
Provedores
Tenant
Escopo
3012
Porta
2025-11
Desde

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.

Para quem é
  • 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
Substitui
  • 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
O que não é
  • 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
O que dá para fazer

15 endpoints em 3 recursos.

Explorar a API →
01

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.

AtributoValor
Identificadorapi-keys
CategoriaIdentidade
EscopoTenant (exige organizationId no token)
Porta (standalone)3012
Path alias@api-keys
Prefixo HTTP/api-keys
StatusProdução desde 2025-11
Depende dePostgreSQL, Redis, IAM

02

O problema

negócio

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


03

Proposta de valor

negócio
AntesDepois
Usuário de serviço com senha em variável de ambienteChave prefixo.segredo, com o segredo guardado só como hash Argon2id
Permissão de administrador porque ninguém quis descobrir o mínimoallowedResources no formato GET /caminho/**, verificado a cada requisição
Rotacionar derruba a integração na horaRotação mantém o prefixo e aceita as duas chaves por até 168 horas
"Quem chamou a API ontem?" é uma suposiçãoUso agregado por hora e por endpoint, consultável por período
A credencial não expira nuncaexpiresAt opcional, verificado na autenticação
Limite de vazão é global ou não existerateLimitMax 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.


04

Casos de uso reais

negócio

Caso 1 — Um marketplace consulta pedidos e não consegue cancelar nenhum Cenário ilustrativo

Contexto

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.

A dor

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.

A solução com o BB

Uma chave por marketplace, cada uma com allowedResources restrito à leitura, rateLimitMax dimensionado ao contrato e expiresAt na data de renovação.

bash
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"
  }'
O resultado

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

Contexto

Financeira que precisa rotacionar as credenciais de integração a cada 90 dias por política interna de segurança.

A dor

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.

A solução com o BB

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 responde
O resultado

A 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

Contexto

Plataforma que quer cobrar por volume de chamadas e, antes disso, precisa saber qual é o volume.

A dor

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.

A solução com o BB

Toda requisição autenticada por chave é contabilizada num balde horário, com detalhamento por endpoint. A consulta é por período.

bash
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 resultado

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

Contexto

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

A dor do mercado

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

Como a Catalisa endereça

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 resultado

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.


05

Mercado e diferenciais

negócio

Panorama. 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érioCatalisa API KeysUnkeyZuploAWS API Gateway
Modelo de preçoIncluso na plataformaPor verificação válida/mês, com camada gratuita de 150 milPor requisição/mês, gratuito até 100 milPor milhão de chamadas
Escopo por rota e métodoSim, padrão MÉTODO /caminhoSim, permissões e rolesVia política do gatewayPor plano de uso, não por rota
Rotação com período de graçaSim, 1 a 168 horasSimDepende da políticaNão nativamente
Limite de vazão por chaveSim, rateLimitMax por janelaSimSimSim, quota e throttling
Uso por endpointSim, balde horário com detalhamentoSim, analytics como produtoSim, analytics por planoCloudWatch
Hash do segredoArgon2idGerenciado pelo fornecedorGerenciado pelo fornecedorGerenciado pelo fornecedor
Amarração ao tenantorganizationId vem do token do IAMVocê modelaVocê modelaVocê modela
Roteamento, cache, transformaçãoNãoNãoSimSim
Portal do desenvolvedorNãoParcialSimVia developer portal
Dependência de rede externa por requisiçãoNão, verificação localSim, chamada ao serviçoSim, é o gatewaySim, é 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

  1. A chave já nasce com o tenant amarrado. organizationId vem 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.
  2. A verificação não sai da sua infraestrutura. O authMiddleware verifica 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".
  3. 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 éEscolhaPor quê
Precisa de gateway completo, com portal do desenvolvedor e transformação de requisiçãoZuplo ou KongO API Keys não roteia nada; ele autentica
Sua API não é da plataforma CatalisaUnkeyNosso 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 throttlingAWS usage plansEstá 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 CatalisaCatalisa API KeysÉ exatamente o recorte implementado

06

Modelo de cobrança e ROI

negócio

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

DriverPor que importaOrdem de grandeza
Chaves ativas por organizaçãoUma linha em api_keys e uma chave no Redis por prefixo verificadoCusto de armazenamento desprezível
Chamadas autenticadas por chaveCada 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_SECONDSO custo real é CPU e memória na verificação fria
Registros de usoUm balde por chave por hora, com detalhamento por endpointCresce 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 KeysUnkeyZuploAWS API Gateway
Base de cálculoIncluso na plataformaVerificações válidas/mêsRequisições/mêsMilhõ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õesGratuito até 100 mil; Builder US$ 25/mês até 1 milhão; Enterprise a partir de US$ 1.000/mêsPor milhão de chamadas, conforme a região
5 milhões de chamadasSem linha adicionalDentro de um plano intermediárioAcima do BuilderCobrado por volume
Latência adicionadaNenhuma chamada externaChamada 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.


07

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&lt;T, AppError&gt;"| 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 --> PW

A 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ção

Decisõ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 — sem 0, o, 1, l e i. 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 PasswordService usado nas senhas do IAM. É caro de propósito, e é por isso que existe cache.
  • O cache guarda o hash, não a chave. CachedApiKey carrega secretHash, 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 requirePermission muda de régua quando a autenticação é por chave. Com JWT, ele compara a permissão exigida contra token.permissions. Com chave, ele ignora o RBAC e compara método e caminho contra allowedResources. São dois modelos de autorização diferentes no mesmo middleware, e é importante saber disso ao escopar uma chave.
  • allowedResources vazio significa acesso total. isResourceAllowed devolve true para 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 trackUsage sem 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.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
Chave completaprefixo.segredo. O único momento em que ela existe inteira é a resposta da criação ou da rotação.
Prefixo8 caracteres em alfabeto sem letras ambíguas. Gravado em claro, é o índice de busca e o identificador em log.
Segredo32 caracteres alfanuméricos. Guardado apenas como hash Argon2id.
Allowed resourcesLista de padrões MÉTODO /caminho. Vazia significa acesso total.
RotaçãoNovo segredo com o mesmo prefixo; o hash anterior sobrevive até gracePeriodEnds.
Período de graçaJanela em que a chave antiga e a nova autenticam. De 1 a 168 horas, padrão 24.
Revogaçãostatus vira REVOKED, revokedAt é preenchido e as rotações pendentes são apagadas.
ExclusãoLógica: deletedAt preenchido, a linha permanece para auditoria.
Balde de usoRegistro por chave e por hora com contagem de requisições, de erros e detalhamento por endpoint.
Admin de chavesQuem 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 PrismaTabelaPropósitoCampos-chave
ApiKeyapikeys.api_keysA chaveprefix, secretHash, status, allowedResources, rateLimitMax, rateLimitWindowMs, expiresAt, lastUsedAt, revokedAt, deletedAt. Únicos (organizationId, prefix) e (organizationId, userId, name)
ApiKeyRotationapikeys.api_key_rotationsHash anterior durante o período de graçaapiKeyId, previousHash, gracePeriodEnds. Cascata ao apagar a chave
ApiKeyUsageRecordapikeys.api_key_usage_recordsUso agregado por horaapiKeyId, 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 note

Enumerações

EnumValoresObservação
ApiKeyStatusACTIVE · REVOKED · EXPIREDEXPIRED 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.

TermoSignifica
SubcontaCliente 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 subcontaChave 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.
environmentRótulo live ou test gravado na chave e exposto no contexto da requisição, para o BB separar sandbox de produção.
X-Subaccount-IdHeader 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:

CredencialX-Subaccount-IdResultado
Chave ou usuário da organizaçãoausentealcance da organização inteira
Chave ou usuário da organizaçãosubconta da própria organizaçãoage como a subconta
Chave ou usuário da organizaçãosubconta de outra organização403 SUBACCOUNT_FORBIDDEN
Chave de subcontaausente ou a mesmaage como a subconta da chave
Chave de subcontaoutra subconta403 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ãoCasa comNão casa com
GET /commerce/api/v1/ordersExatamente 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 daliOutro método
* /customers/api/v1/persons/**Qualquer método sob esse caminhoOutro 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.


09

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étodoRotaDescriçãoPermissão
POST/api-keys/api/v1/api-keysCria a chave e devolve o valor completo, uma única vezAPI_KEYS_CREATE
GET/api-keys/api/v1/api-keysLista as chaves, paginadoAPI_KEYS_READ
GET/api-keys/api/v1/api-keys/:keyIdBusca uma chave (sem o segredo)API_KEYS_READ
PATCH/api-keys/api/v1/api-keys/:keyIdAtualiza nome, escopos, limites e expiraçãoAPI_KEYS_UPDATE
POST/api-keys/api/v1/api-keys/:keyId/rotateGera novo segredo com período de graçaAPI_KEYS_ROTATE
POST/api-keys/api/v1/api-keys/:keyId/revokeRevoga a chave imediatamenteAPI_KEYS_DELETE
DELETE/api-keys/api/v1/api-keys/:keyIdExclusão lógicaAPI_KEYS_DELETE
GET/api-keys/api/v1/api-keys/:keyId/usageEstatísticas de uso por períodoAPI_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étodoRotaDescriçãoPermissão
POST/api-keys/api/v1/subaccountsCria a subconta (name, externalRef?, monthlyQuota?, metadata?); externalRef repetido na organização é 409SUBACCOUNTS_CREATE
GET/api-keys/api/v1/subaccountsLista, paginado; filter[status], filter[externalRef] e search (nome ou externalRef)SUBACCOUNTS_READ
GET/api-keys/api/v1/subaccounts/:idBusca uma subcontaSUBACCOUNTS_READ
PATCH/api-keys/api/v1/subaccounts/:idNome, externalRef, monthlyQuota, metadata, status e suspendedReasonSUBACCOUNTS_UPDATE
POST/api-keys/api/v1/subaccounts/:id/api-keysEmite chave da subconta (mesmo corpo da criação de chave)API_KEYS_CREATE
GET/api-keys/api/v1/subaccounts/:id/api-keysChaves da subconta, paginadoAPI_KEYS_READ

Suspender grava suspendedAt; reativar (status: "ACTIVE") limpa suspendedAt e suspendedReason. Emitir chave para subconta suspensa é 403 SUBACCOUNT_SUSPENDED.

Saúde

MétodoRotaDescrição
GET/api-keys/healthSonda 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

json
{
  "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"
}
CampoTipoObrigatórioDescrição
namestring (1–100)SimÚnico por (organização, usuário). Colisão devolve 409
descriptionstring (≤ 500)NãoTexto livre
allowedResourcesstring[]NãoPadrões MÉTODO /caminho. Omitido ou vazio libera tudo
rateLimitMaxint (1–1.000.000)NãoSem valor, não há limite por chave
rateLimitWindowMsint (1–86.400.000)NãoJanela do limite. Precisa vir junto com rateLimitMax
expiresAtdateNãoSem valor, a chave não expira

Resposta 201

json
{
  "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.

ErroQuando
400 VALIDATIONPadrão de allowedResources fora do formato MÉTODO /caminho, limite fora da faixa
403Token sem organizationId ou sem API_KEYS_CREATE
409 CONFLICTJá 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)

json
{ "gracePeriodHours": 72 }
{ "gracePeriodHours": 72 }
CampoTipoObrigatórioDescrição
gracePeriodHoursint (1–168)NãoPadrão 24. Quanto tempo a chave antiga continua aceita

Resposta 200

json
{
  "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 queryDescriçã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 queryTipoObrigatórioDescrição
startDatedateSimInício do período. Ausente devolve 400
endDatedateSimFim do período. Ausente devolve 400

Resposta 200

json
{
  "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:

bash
# 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çãoSignifica
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

10

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.

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

bash
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 mostrar
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 mostrar

Esperado: 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.

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

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

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

bash
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "https://api-keys.bb.stg.catalisa.app/api-keys/api/v1/api-keys/$KEY_ID/revoke" \
  -H "Authorization: Bearer $TOKEN" | jq

Esperado: { "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.


11

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

bash
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 allowedResources libera a API inteira. Lista vazia ou ausente devolve true na 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. É o pathname da requisição que é comparado.
  • * casa um segmento e ** casa o resto. GET /orders/* não cobre /orders/123/items.
  • rateLimitMax sem rateLimitWindowMs nã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

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

bash
# 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 }'
bash
# 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.

bash
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

bash
# 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:

  1. O status é ACTIVE? Se não, é 401, não 403.
  2. O caminho que o parceiro chama é exatamente o que está no padrão, com o prefixo do building block?
  3. O método bate? GET /rota não cobre HEAD nem POST.
  4. O curinga é o certo? Um * a menos e /orders/123/items não casa.
  5. Se tudo bate, é limite de vazão — a mensagem do 403 diz Retry after N seconds.

Auditar quais chaves ainda estão em uso

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


12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token que autoriza criar e gerir chaves, e define o organizationId que a chave herdaSim
Todos os demaisAceitam autenticação por chave pelo authMiddleware compartilhado, sem código próprio—
Audit TrailConsome apikeys.key.created, apikeys.key.updated, apikeys.key.rotated e apikeys.key.revokedNão
Webhooks EnginePode entregar esses eventos a sistemas externosNão
SSOCaminho paralelo, para pessoa em vez de máquinaNã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ãoIAM (JWT)API Keys
Vida da credencial1 hora, renovávelLonga, até expiresAt ou revogação
Modelo de autorizaçãoRBAC — permissões no tokenPadrões MÉTODO /caminho na chave
Onde a autorização é verificadarequirePermission contra token.permissionsrequirePermission contra allowedResources
RevogaçãoSó na renovação do tokenImediata, com a ressalva do cache da §11
Limite de vazãoGlobal e nas rotas de autenticaçãoPor chave, configurável
Medição de usoNãoSim, por hora e por endpoint
Melhor paraIntegração viva, com escopo que mudaIntegraçã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.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
API_KEY_CACHE_TTL_SECONDSVida no Redis da chave já verificada. É o que define a janela em que uma revogação ainda não surtiu efeitoNão300
DATABASE_URLPostgreSQL, schema apikeysSim—
REDIS_URLRedis, usado no cache de verificação e no limite de vazãoSim—
JWT_SECRETSegredo HS256 do IAM, para as rotas administrativasSim—
PORTPorta em standaloneNão3012 conforme DEFAULT_MODULE_PORTS
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

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ênciaPara quêSe cair
PostgreSQL (schema apikeys)Chaves, rotações e usoNenhuma chave autentica em nenhum building block
RedisCache de verificação e limite de vazão por chaveToda verificação vira Argon2id frio, o que é lento; sob carga vira gargalo
IAMToken das rotas administrativasNão dá para criar nem gerir chaves; as existentes continuam autenticando

Limites e quotas

LimiteValorOnde
Tamanho do prefixo8 caracteres, alfabeto de 31 símbolosConstante em key-generator.ts
Tamanho do segredo32 caracteres base62Constante em key-generator.ts
Nome da chave1 a 100 caracteres, único por (organização, usuário)Zod + único no banco
DescriçãoAté 500 caracteresZod
rateLimitMax1 a 1.000.000Zod
rateLimitWindowMs1 a 86.400.000 (24 horas)Zod
Período de graça1 a 168 horas, padrão 24Zod
PaginaçãopageSize máximo 100Zod
Corpo da requisição1 MBapplyCommonMiddleware
Tentativas de gerar prefixo único10, depois falha com erro de validaçãogenerateUniquePrefix

Catálogo de erros

CódigoSignificaO 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 revogadaConfira o formato em §9
UNAUTHORIZED (401)Chave em formato inválido, inexistente, revogada ou expiradaConfira 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 estouradoVer o diagnóstico em §11
NOT_FOUND (404)Chave inexistente nesta organizaçãoListe antes de operar por ID
CONFLICT (409)Nome de chave repetido para o mesmo usuárioEscolha outro nome
INTERNAL (500)Falha ao gerar hash, gravar ou publicar eventoVerifique Postgres e Redis

Observabilidade

SinalOndePara quê
apikeys.key.created · key.updated · key.rotated · key.revokedEventos publicados, com apiKeyId, prefix e nameTrilha de auditoria do ciclo de vida
PERMISSION_DENIED no log de segurançaEmitido pelo requirePermission com o prefixo da chaveInvestigar chave tentando o que não pode, sem precisar da chave
ApiKeyUsageRecordBalde horário por chave, com detalhamento por endpointConsumo e dimensionamento
lastUsedAtCampo da chaveEncontrar credencial ociosa
cleanupOldRecords(retentionDays)Método do ApiKeyUsageServiceExpurgo de registros antigos. Não há agendador chamando isso — ver §15

14

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"| F

Nas 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

DadoTratamento
Segredo da chaveHash 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
PrefixoGravado 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çãoGravado em api_key_rotations e apagado quando a chave é revogada ou excluída
Chave em cacheO Redis guarda o hash, status, escopos e limites — nunca a chave. Um dump do cache não devolve credencial
Chave em logO log de segurança registra apiKeyPrefix, não a chave
Registros de usoContêm método, caminho e contagens. Não contêm corpo de requisição nem parâmetro

Autenticação e permissões exigidas

PermissãoConcede
API_KEYS_CREATECriar chave
API_KEYS_READListar e ler chaves (nunca o segredo)
API_KEYS_UPDATEAlterar nome, escopos, limites e expiração
API_KEYS_ROTATERotacionar
API_KEYS_DELETERevogar e excluir. Também confere visão administrativa sobre as chaves dos outros usuários da organização
API_KEYS_READ_USAGEConsultar 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: userId de 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 allowedResources e expiresAt. 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 lastUsedAt e revogue o que está ocioso. Credencial esquecida é a que ninguém percebe sendo usada.

15

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.


16

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.