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.
| Credential | Use |
|---|---|
| api key | Identifies your integration. It goes in the Authorization header of every request. |
| secret key | Generates 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>
Authorizationidentifies your integration and carries the request signature, computed as described below.x-company-internal-codeindicates 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
- Build the signed text, one item per line, separated by
\n(an actual line break, not the characters\andn):- HTTP method in uppercase (
GET,POST,PUT,PATCH); - URL path, without host or query string (
/materials/boards); an empty path counts as/; - query string parameters sorted by name (and, for repeated names, by value), each as
name=valueencoded as JavaScript'sencodeURIComponentdoes, joined by&; without a query string, an empty line; host:<host>followed by a blank line, where<host>is exactly the value your HTTP library sends in thehostheader (the hostname, plus:portonly when the port is not the protocol's default);- the word
host, which is the list of signed headers; - hexadecimal SHA-256 of the body bytes; without a body, the SHA-256 of the empty string (
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).
- HTTP method in uppercase (
- Calculate the HMAC-SHA256 of this text using the secret key as the key.
- 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:
| Example | Signature |
|---|---|
| GET | e2a240cea084ff4950cc78e1d48d6da563aae9f7935a8dfaa28c3672598ef344 |
| POST | 8ac32656f8233168640afaff2f7c6213e42dd0fb9cdbd3f81b2ae666cab94e2b |
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:
Authorizationorx-company-internal-codeis missing or malformed;- the api key is unknown or has been revoked;
- the service center indicated in
x-company-internal-codeis not linked to the api key or is not active on Cortecloud; signed-headersnames 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.