Authentication and signed calls
Send your key in the X-API-Key header. Keys made with "Sign every call" also need a timestamp and an HMAC-SHA256 signature, with examples in curl, Python and Node.js.
Every call to a check carries your key. A key made with Sign every call also needs a fresh signature on each call.
The key#
Send the key in the X-API-Key header of every call:
POST /api/ip-lookup HTTP/1.1
Host: service.cerberius.com
X-API-Key: cbk_…
Content-Type: application/json
{"data": ["8.8.8.8"]}
That's all an ordinary key needs. Keys start with cbk_. Send the whole key, including that start.
A call with no key, or with a key that isn't valid or was removed, gets a 401 error. See Errors and limits.
Signed calls#
When you make a key under Organisation › API, you can tick Sign every call. The key then comes with a second secret, the signing secret. The window after making the key shows both, once.
Each call with that key needs two more headers:
| Header | What to send |
|---|---|
X-API-Key | Your key, as for any call. |
X-Timestamp | The time now, in whole seconds since 1 January 1970 UTC (Unix time), such as 1762675200. It must be within 5 minutes of our clock. |
X-Signature | The HMAC-SHA256 of the timestamp followed by the key, made with the signing secret, written as lowercase hex (64 characters). |
As a formula:
X-Signature = hex( HMAC-SHA256( signing secret, X-Timestamp + X-API-Key ) )
The signing secret is the HMAC's secret key. The + joins the two texts with nothing between them. With the timestamp 1762675200 and the key cbk_Ab12…, you sign the text 1762675200cbk_Ab12….
What this gives you:
- The key alone isn't enough to call the API. Whoever calls also needs the signing secret, and the secret itself is never sent.
- A call someone copies stops working 5 minutes after its timestamp.
Make a new timestamp and signature for every call. Don't make one when your program starts and reuse it.
You can't switch signing on or off for an existing key. Make a new key, move your system to it, then remove the old key. A key shows Signed calls next to its name in the Keys table when it needs signing.
Lost the signing secret?
It can't be shown again. Remove the key and make a new one.
Examples#
Each example signs one IP lookup and prints the country. They read the key from CERBERIUS_API_KEY and the signing secret from CERBERIUS_SIGNING_SECRET.
curl#
Uses openssl, which comes with macOS and most Linux systems.
KEY="$CERBERIUS_API_KEY"
SECRET="$CERBERIUS_SIGNING_SECRET"
TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s' "$TIMESTAMP$KEY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl https://service.cerberius.com/api/ip-lookup \
-H "X-API-Key: $KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"data": ["8.8.8.8"]}'
Python#
Needs the requests package (pip install requests).
import hashlib
import hmac
import os
import time
import requests
API = "https://service.cerberius.com/api"
KEY = os.environ["CERBERIUS_API_KEY"]
SECRET = os.environ["CERBERIUS_SIGNING_SECRET"]
def signed_headers() -> dict[str, str]:
timestamp = str(int(time.time()))
signature = hmac.new(SECRET.encode(), (timestamp + KEY).encode(), hashlib.sha256).hexdigest()
return {"X-API-Key": KEY, "X-Timestamp": timestamp, "X-Signature": signature}
response = requests.post(
f"{API}/ip-lookup",
headers=signed_headers(),
json={"data": ["8.8.8.8"]},
timeout=100,
)
answer = response.json()
if response.status_code != 200:
raise RuntimeError(f"{response.status_code}: {answer['error']['message']}")
print(answer["data"][0]["country"])
Node.js#
Needs Node.js 18 or later. Save it as a .mjs file.
import { createHmac } from "node:crypto";
const API = "https://service.cerberius.com/api";
const KEY = process.env.CERBERIUS_API_KEY;
const SECRET = process.env.CERBERIUS_SIGNING_SECRET;
function signedHeaders() {
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", SECRET).update(timestamp + KEY).digest("hex");
return { "X-API-Key": KEY, "X-Timestamp": timestamp, "X-Signature": signature };
}
const response = await fetch(`${API}/ip-lookup`, {
method: "POST",
headers: { "Content-Type": "application/json", ...signedHeaders() },
body: JSON.stringify({ data: ["8.8.8.8"] }),
signal: AbortSignal.timeout(100_000),
});
const answer = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${answer.error.message}`);
console.log(answer.data[0].country);
Common mistakes#
A signed call that doesn't hold gets a 401 error. Its message says what's wrong:
| Message | Usual cause |
|---|---|
| This key needs every call signed: send X-Timestamp … | The key was made with Sign every call, and the call has no X-Timestamp or no X-Signature. |
| X-Timestamp must be the time in seconds since 1970. | The timestamp isn't a number. A date such as 2026-11-09T10:00:00Z doesn't work. |
| X-Timestamp is more than 5 minutes away from now. Check your clock. | Your server's clock is wrong, or the timestamp was made long before the call. |
| X-Signature doesn't match. Sign the timestamp followed by the key. | See the checklist below. |
When the signature doesn't match, check:
- The order. Sign the timestamp first, then the key. Not the key, then the timestamp.
- The same timestamp. Sign exactly the text you send in
X-Timestamp. A timestamp in milliseconds also works, as long as you sign the same text you send. - The right secret. Make the HMAC with the signing secret, not with the key.
- Hex, not base64. The signature is 64 characters,
0to9andatof. - Nothing extra.
echoadds a line break to the signed text. Useprintf '%s', as in the curl example. Don't put spaces or quotes inside the values. - The whole key, including
cbk_at the start.