Pular para o conteúdo

Chaves, escopos e restrições

Existem duas classes de chave de API, uma lista fixa de escopos e quatro restrições opcionais que o dono de uma chave pode anexar a ela. As três coisas são definidas quando a chave é criada, no admin, em Configurações → Integrações API.

Chave secreta Chave de leitura pública
Prefixo ck_ pk_
Onde fica Só no servidor — variável de ambiente, gerenciador de segredos No HTML da página que incorpora um widget
Pode escrever Sim Não, nunca
Enviada como Authorization: Bearer ck_… Authorization: Bearer pk_… ou ?key=pk_…
Utilizável a partir do navegador Não — as rotas de leitura propositalmente não devolvem cabeçalhos CORS para uma chave ck_ Sim, só a partir de origens registradas
Allowlist de origens Não se aplica (rejeitada na criação) Obrigatória, de 1 a 20 origens

As duas são o prefixo seguido de 32 caracteres base62 (≈190 bits de entropia) e as duas são exibidas uma única vez, na criação. A Canverly armazena apenas um hash SHA-256. As chaves são opacas: não as analise nem as divida.

Uma chave de leitura pública só pode ser criada com escopos de leitura: pedir posts:write nela é um 400 na criação. Na prática, a superfície que ela alcança são as duas rotas públicas de leitura:

Todas as outras rotas autenticam apenas tokens ck_ e respondem 401 a uma chave pk_, mesmo quando a chave carrega o escopo correspondente. Considere post_types:read e sites:read numa chave pública como inertes hoje.

Os escopos são opt-in por chave: uma chave recebe exatamente o que foi pedido na criação, e nada por padrão. Estes são os catorze que a API reconhece; qualquer outro é rejeitado quando a chave é emitida.

Escopo Concede
posts:write POST /v1/posts, PATCH /v1/posts/{id}
posts:read GET /v1/posts, GET /v1/posts/{reference}
post_types:read GET /v1/post-types
sites:read GET /v1/sites/me
site:read GET /v1/sites/me/settings
site:write PATCH /v1/sites/me/settings
seo:read GET /v1/posts/{reference}/seo, GET /v1/sites/me/seo
seo:write PATCH /v1/posts/{reference}/seo, PATCH /v1/sites/me/seo
media:read GET /v1/media, GET /v1/media/{id}
media:write POST /v1/media, DELETE /v1/media/{id}
analytics:read GET /v1/analytics/summary, GET /v1/analytics/posts
leads:read GET /v1/leads, GET /v1/leads/{id}
export GET /v1/export
import POST /v1/import

Pontos que costumam pegar as pessoas de surpresa:

  • sites:read ≠ site:read. O plural é a chamada de identidade; o singular é o bloco de configurações. A diferença não é erro de digitação.
  • Leitura e escrita são sempre separadas. Uma chave com seo:read nunca consegue escrever SEO. Isso vale por par, de propósito.
  • Algumas rotas exigem dois escopos. POST /v1/import exige import e posts:write. GET /v1/export?resource=leads exige export e leads:read; ?resource=analytics exige export e analytics:read.
  • leads:read são dados pessoais. Nunca é concedido por padrão, toda leitura é gravada na trilha de auditoria com a chave como autora, e ele tem um bucket próprio de 10 req/min além do limite geral.

Uma requisição a uma rota cujo escopo a chave não tem é um 403 que nomeia o escopo:

{ "error": { "code": "forbidden", "message": "scope `posts:write` not granted" } }

Além dos escopos, uma chave pode carregar até quatro restrições. Elas são avaliadas por requisição, em chaves ck_ e pk_, em todo o /v1, e antes de o bucket do limite de requisição ser cobrado, então uma requisição bloqueada não consome cota. Uma chave sem restrições pula a verificação inteira.

Uma dimensão vazia não restringe. Uma dimensão que está definida e não pode ser avaliada nega: uma restrição que falha aberta é uma restrição que não existe justamente no momento em que mais importa.

Restrição Aceita Máximo de entradas
Allowlist de IP Endereços exatos ou faixas CIDR — 203.0.113.4, 2001:db8::/32 64
Allowlist de região Códigos de país ISO 3166-1 alpha-2, em maiúsculas — BR, US 250
Fuso horário Um fuso IANA — America/Sao_Paulo 1
Janelas de horário Conjunto de dias da semana + intervalo HH:MM–HH:MM, interpretado nesse fuso 32
  • IP: se o endereço do cliente não puder ser determinado, uma allowlist de IP ativa nega.
  • Região: resolvida por GeoIP a partir do endereço do cliente. Um país indeterminado (endereço privado, faixa desconhecida, falha na consulta) nega quando há uma allowlist de região definida. Isso nunca é lido como “sem restrição”.
  • Janelas de horário: a requisição passa se cair dentro de qualquer janela. Os dias usam as formas de três letras minúsculas mon … sun; end precisa ser estritamente posterior a start. As janelas precisam de um fuso horário para significar algo: sem ele, elas não restringem.

Uma requisição bloqueada é um 403 que nomeia a dimensão e nunca a lista:

{ "error": { "code": "forbidden",
"message": "request blocked by this key's IP restriction" } }

As outras duas mensagens são request blocked by this key's region restriction e request blocked by this key's time-window restriction.

Uma chave pk_ é verificada contra o cabeçalho Origin do navegador, e a comparação é byte a byte sobre uma forma normalizada. As regras, cada uma existente por causa de uma forma conhecida de errar:

  • Só https, exceto http://localhost, http://127.0.0.1 e http://[::1]: você precisa conseguir testar o widget na sua máquina.
  • Sem curingas. https://*.example.com delegaria para qualquer subdomínio, inclusive um que um atacante registre num serviço de páginas de terceiros sob o domínio do cliente.
  • Sem path, query, fragmento ou userinfo. Uma “origem” com path nunca corresponde ao que o navegador envia, e você ficaria depurando uma lista que parece certa.
  • Portas padrão são removidas da forma armazenada, porque é assim que o navegador serializa o Origin (RFC 6454): uma página em https://x.example:443 envia Origin: https://x.example.
  • Origin: null (frames em sandbox, file://) é rejeitado como entrada e nunca corresponde.
  • Até 20 origens por chave, deduplicadas após a normalização.

Uma origem não registrada recebe 403: a chave é válida, a página não é.

Bucket Limite Aplica-se a
Escrita / geral 60 req/min por chave Todas as rotas, exceto as duas rotas públicas de leitura
Leitura 600 req/min por chave GET /v1/posts, GET /v1/posts/{reference}
Leitura por (chave, IP) 60 req/min As mesmas duas rotas, só para chaves pk_
Leads 10 req/min por chave GET /v1/leads, GET /v1/leads/{id}, exportação de leads — além do bucket geral

Ler é dez vezes mais barato que escrever, de propósito: compartilhar um bucket deixaria uma grade de widgets movimentada consumir a cota de publicação do dono. Veja Erros e limites de requisição.

  • Uma chave por integração. Revogue um único consumidor sem quebrar os outros; o last_used_at mostra quais chaves ainda estão vivas.
  • Nunca faça commit de uma chave ck_ nem a coloque em código client-side. Se uma vazar, revogue-a imediatamente: a revogação vale a partir da próxima requisição.
  • Rotacione sem indisponibilidade: crie uma nova chave com os mesmos escopos, mude a integração, confirme e depois revogue a antiga.
  • Peça o menor conjunto de escopos que funcione. leads:read e site:write, em especial, devem ficar em chave própria, não juntos na chave de publicação.

A interface do admin se apoia em quatro rotas (POST e GET /admin/api-keys, PATCH e DELETE /admin/api-keys/{id}), que definem exatamente o que esta página descreve: nome, classe, escopos, origens e as quatro restrições.

O que um PATCH pode mudar: a política — escopos, origens e as restrições de IP, região e janela de horário. O que ele não pode mudar: o segredo. A plataforma armazena apenas um SHA-256 da sua chave, então ninguém, nem mesmo a interface do admin, consegue exibi-la de novo ou reemiti-la. Essa assimetria é o ponto: corrigir um escopo faltante não deveria obrigar você a trocar o segredo em toda integração que o usa. Mudar o que uma chave pode fazer não muda qual chave ela é.

Uma regra sobrevive à edição: uma chave pk_ nunca pode receber um escopo de escrita, a mesma recusa que você recebe na criação. Do contrário, a garantia de somente leitura seria uma formalidade: criar uma leitora e promovê-la depois.

Elas ficam fora do contrato /v1 do /openapi.json pelo mesmo motivo.