Autenticação
Toda requisição à API é assinada com HMAC-SHA256 no esquema SB1-HMAC-SHA256. Não há login, sessão, cookie nem token para renovar: a mesma secret key assina todas as chamadas até ser trocada.
Credenciais
A Serrabits fornece à sua integração duas credenciais por ambiente. Para obtê-las, escreva para suporte@serrabits.com.br.
| Credencial | Uso |
|---|---|
| api key | Identifica a sua integração. Vai no cabeçalho Authorization de toda requisição. |
| secret key | Gera a assinatura de cada requisição. Nunca é enviada: só a assinatura vai na requisição. |
A secret key deve ficar apenas no seu backend. Não a coloque em código de frontend, app mobile ou qualquer lugar acessível pelo navegador do usuário: quem tiver a secret key consegue fazer chamadas em nome da sua integração. Se ela vazar, escreva para suporte@serrabits.com.br para revogá-la e receber uma nova.
Cabeçalhos
Toda requisição leva dois cabeçalhos:
Authorization: SB1-HMAC-SHA256 api-key="<api key>", signed-headers="host", signature="<assinatura>"
x-company-internal-code: <código da central>
Authorizationidentifica a sua integração e traz a assinatura da requisição, calculada como descrito abaixo.x-company-internal-codeindica a central sobre a qual a requisição atua (ver código da central). Esse cabeçalho não entra na assinatura.
Requisições com corpo também enviam Content-Type: application/json.
A assinatura cobre método, caminho, query string, host e corpo. Alterar qualquer um deles depois de assinar invalida a requisição, e uma requisição capturada não pode ser reaproveitada com outro caminho, método ou corpo.
Como calcular a assinatura
- Monte o texto a ser assinado, com uma informação por linha, separadas por
\n(quebra de linha real, não os caracteres\en):- método HTTP em maiúsculas (
GET,POST,PUT,PATCH); - caminho da URL, sem host nem query string (
/materials/boards); caminho vazio vale/; - parâmetros da query string ordenados por nome (e, em nomes repetidos, por valor), cada um como
nome=valorcodificado como noencodeURIComponentdo JavaScript, unidos por&; sem query string, linha vazia; host:<host>seguido de uma linha em branco, onde<host>é exatamente o valor que a sua biblioteca HTTP envia no cabeçalhohost(o hostname, mais:portasó quando a porta não é a padrão do protocolo);- a palavra
host, que é a lista de cabeçalhos assinados; - SHA-256 em hexadecimal dos bytes do corpo; sem corpo, o SHA-256 da string vazia (
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).
- método HTTP em maiúsculas (
- Calcule o HMAC-SHA256 desse texto com a secret key como chave.
- Use o resultado em hexadecimal minúsculo como
<assinatura>.
A linha em branco depois de host:<host> faz parte do texto: a linha do host termina com a sua própria quebra de linha, somada ao \n que separa as partes.
Exemplo: GET
Texto assinado para GET /materials/boards?limit=10 no host api.example.com, sem corpo:
GET
/materials/boards
limit=10
host:api.example.com
host
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Exemplo: POST
Texto assinado para POST /services/123/production no host api.example.com, com o corpo {"sellerEmail":"vendedor@example.com"}. O hash é calculado sobre os bytes exatos do corpo enviado: reformatar o JSON depois de assinar (espaços, quebras de linha, ordem das chaves) invalida a assinatura.
POST
/services/123/production
host:api.example.com
host
d4f22123a7a0685bd98bb9f7fc9b3db455e5b4e0c4420f0e0ddedd4826a3e0cc
Sem query string, a terceira linha fica vazia.
Valide a sua implementação
Com a secret key de teste sb_sk_exemplo, os dois textos acima produzem estas assinaturas:
| Exemplo | Assinatura |
|---|---|
| GET | e2a240cea084ff4950cc78e1d48d6da563aae9f7935a8dfaa28c3672598ef344 |
| POST | 8ac32656f8233168640afaff2f7c6213e42dd0fb9cdbd3f81b2ae666cab94e2b |
Se a sua implementação chega nos mesmos valores, ela está montando o texto do jeito que a API espera.
Código de referência
Node.js
const crypto = require('crypto');
function canonicalQueryString(url) {
const entries = [...url.searchParams.entries()].sort(([ak, av], [bk, bv]) =>
ak === bk ? av.localeCompare(bv) : ak.localeCompare(bk),
);
return entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
}
function buildCanonicalRequest(method, url, body = '') {
const payloadHash = crypto.createHash('sha256').update(body, 'utf-8').digest('hex');
return [
method.toUpperCase(),
url.pathname || '/',
canonicalQueryString(url),
`host:${url.host}\n`,
'host',
payloadHash,
].join('\n');
}
const url = new URL('https://api.example.com/materials/boards?limit=10');
const canonicalRequest = buildCanonicalRequest('GET', url);
const signature = crypto.createHmac('sha256', secretKey).update(canonicalRequest).digest('hex');
const authorization = `SB1-HMAC-SHA256 api-key="${apiKey}", signed-headers="host", signature="${signature}"`;
Python
# urllib.parse.quote com safe="!*'()" reproduz o mesmo conjunto
# de caracteres que o encodeURIComponent do JavaScript deixa sem escapar.
import hashlib
import hmac
import urllib.parse
def encode(value):
return urllib.parse.quote(str(value), safe="!*'()")
def canonical_query_string(query_pairs):
return "&".join(
f"{encode(k)}={encode(v)}"
for k, v in sorted(query_pairs, key=lambda kv: (kv[0], kv[1]))
)
def build_canonical_request(method, path, query_pairs, host, body=b""):
payload_hash = hashlib.sha256(body).hexdigest()
return "\n".join([
method.upper(),
path or "/",
canonical_query_string(query_pairs),
f"host:{host}\n",
"host",
payload_hash,
])
canonical_request = build_canonical_request("GET", "/materials/boards", [("limit", "10")], "api.example.com")
signature = hmac.new(secret_key.encode(), canonical_request.encode(), hashlib.sha256).hexdigest()
authorization = f'SB1-HMAC-SHA256 api-key="{api_key}", signed-headers="host", signature="{signature}"'
Não defina o cabeçalho host manualmente na chamada HTTP: a biblioteca já o envia a partir da URL. Garanta só que o valor usado para assinar (url.host e host nos exemplos) seja exatamente o que ela envia; na maioria das bibliotecas, é o mesmo.
Para requisições com corpo, assine e envie os mesmos bytes. Serialize o JSON uma vez, guarde a string e use-a tanto no hash quanto no corpo da requisição.
Quando a autenticação falha
A API responde 401 quando:
Authorizationoux-company-internal-codefalta ou está malformado;- a api key é desconhecida ou foi revogada;
- a central indicada em
x-company-internal-codenão está vinculada à api key ou não está ativa no Cortecloud; signed-headersnomeia um cabeçalho que a requisição não enviou;- a assinatura não confere com a requisição recebida.
Para diagnosticar uma assinatura que não confere, compare o texto que você assinou com o formato acima, linha a linha: as causas mais comuns são a falta da linha em branco depois do host, a query string fora de ordem e o corpo reformatado entre o hash e o envio.