More in API

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.

In the appOrganisationAPIMake a key

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:

HeaderWhat to send
X-API-KeyYour key, as for any call.
X-TimestampThe 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-SignatureThe 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:

MessageUsual 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, 0 to 9 and a to f.
  • Nothing extra. echo adds a line break to the signed text. Use printf '%s', as in the curl example. Don't put spaces or quotes inside the values.
  • The whole key, including cbk_ at the start.