Skip to main content

Authentication

Every request to the API is signed with HMAC-SHA256 under the SB1-HMAC-SHA256 scheme. There is no login, session, cookie or token to renew: the same secret key signs every call until it is replaced.

Credentials​

Serrabits provides your integration with two credentials per environment. To get them, write to suporte@serrabits.com.br.

CredentialUse
api keyIdentifies your integration. It goes in the Authorization header of every request.
secret keyGenerates the signature of each request. It is never sent: only the signature goes in the request.

The secret key must stay in your backend only. Do not put it in frontend code, a mobile app or anywhere reachable from the user's browser: whoever has the secret key can make calls on behalf of your integration. If it leaks, write to suporte@serrabits.com.br to have it revoked and receive a new one.

Headers​

Every request carries two headers:

Authorization: SB1-HMAC-SHA256 api-key="<api key>", signed-headers="host", signature="<signature>"
x-company-internal-code: <service center code>
  • Authorization identifies your integration and carries the request signature, computed as described below.
  • x-company-internal-code indicates the service center the request acts on (see service center code). This header is not part of the signature.

Requests with a body also send Content-Type: application/json.

The signature covers method, path, query string, host and body. Changing any of them after signing invalidates the request, and a captured request cannot be reused with a different path, method or body.

How to calculate the signature​

  1. Build the signed text, one item per line, separated by \n (an actual line break, not the characters \ and n):
    1. HTTP method in uppercase (GET, POST, PUT, PATCH);
    2. URL path, without host or query string (/materials/boards); an empty path counts as /;
    3. query string parameters sorted by name (and, for repeated names, by value), each as name=value encoded as JavaScript's encodeURIComponent does, joined by &; without a query string, an empty line;
    4. host:<host> followed by a blank line, where <host> is exactly the value your HTTP library sends in the host header (the hostname, plus :port only when the port is not the protocol's default);
    5. the word host, which is the list of signed headers;
    6. hexadecimal SHA-256 of the body bytes; without a body, the SHA-256 of the empty string (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).
  2. Calculate the HMAC-SHA256 of this text using the secret key as the key.
  3. Use the result in lowercase hexadecimal as <signature>.

The blank line after host:<host> is part of the text: the host line ends with its own line break, added to the \n that separates the parts.

Example: GET​

Signed text for GET /materials/boards?limit=10 on host api.example.com, without a body:

GET
/materials/boards
limit=10
host:api.example.com

host
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Example: POST​

Signed text for POST /services/123/production on host api.example.com, with the body {"sellerEmail":"vendedor@example.com"}. The hash is calculated over the exact bytes of the body sent: reformatting the JSON after signing (spaces, line breaks, key order) invalidates the signature.

POST
/services/123/production

host:api.example.com

host
d4f22123a7a0685bd98bb9f7fc9b3db455e5b4e0c4420f0e0ddedd4826a3e0cc

Without a query string, the third line is empty.

Validate your implementation​

With the test secret key sb_sk_exemplo, the two texts above produce these signatures:

ExampleSignature
GETe2a240cea084ff4950cc78e1d48d6da563aae9f7935a8dfaa28c3672598ef344
POST8ac32656f8233168640afaff2f7c6213e42dd0fb9cdbd3f81b2ae666cab94e2b

If your implementation arrives at the same values, it is building the text the way the API expects.

Reference code​

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 with safe="!*'()" reproduces the same set
# of characters that JavaScript's encodeURIComponent leaves unescaped.
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}"'

Do not set the host header manually in the HTTP call: the library already sends it based on the URL. Just make sure the value used for signing (url.host and host in the examples) is exactly what it sends; in most libraries, it is the same.

For requests with a body, sign and send the same bytes. Serialize the JSON once, keep the string and use it both in the hash and in the request body.

When authentication fails​

The API responds with 401 when:

  • Authorization or x-company-internal-code is missing or malformed;
  • the api key is unknown or has been revoked;
  • the service center indicated in x-company-internal-code is not linked to the api key or is not active on Cortecloud;
  • signed-headers names a header the request did not send;
  • the signature does not match the received request.

To diagnose a signature that does not match, compare the text you signed with the format above, line by line: the most common causes are the missing blank line after the host, the query string out of order and the body reformatted between hashing and sending.