Skip to main content

Authentication — signing requests

Every Tenant API request is signed with your credential's secret. The secret never travels: IDM checks that the signature matches what it computes from the same request.

The four headers​

HeaderValue
X-Intellisol-Key-IdYour key ID, for example pk_live_…
X-Intellisol-TimestampThe current Unix time in whole seconds
X-Intellisol-NonceA fresh random string, 16 to 64 characters of A–Z a–z 0–9 _ -, never reused
AuthorizationSignature keyId="<key id>",algorithm="hmac-sha256",signature="<hex>"

What you sign​

Build the canonical string from five lines joined by a single newline (\n), with no newline at the end:

<METHOD in upper case>
<request URI: the path and query string exactly as sent>
<lower-case hex SHA-256 of the raw request body; of the empty string when there is no body>
<the timestamp header value>
<the nonce header value>

The signature is the lower-case hex HMAC-SHA256 of that string, keyed with your secret exactly as it was issued (the whole sk_… string).

Sign the bytes you send

Serialise the JSON body once, sign those bytes and send those same bytes. Re-serialising after signing (a pretty-printer, a different key order) changes the digest and the signature fails.

Test your implementation​

With these inputs your code must produce exactly this signature:

InputValue
MethodPOST
Request URI/v1/delivery/quote?x=1
Body{"a":1}
Timestamp1789312301
Nonce7c1e0a6b5d4f3a2b1c0d
Secretsk_test_secret
Body digest015abd7f5cc57a2dd94b7590f04ad8084273905ee33ec5cebeae62276a97f862
Signature059168a68799f8d2c27ec2497d63bc5b0e89aaa4c927c4161010c504ef137276

The digest of an empty body is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

Code​

Node.js​

import crypto from 'node:crypto';

export async function idmFetch(host, method, uri, bodyObject, { keyId, secret, idempotencyKey } = {}) {
const body = bodyObject === undefined ? '' : JSON.stringify(bodyObject);
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
const digest = crypto.createHash('sha256').update(body).digest('hex');
const canonical = [method.toUpperCase(), uri, digest, timestamp, nonce].join('\n');
const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');

const headers = {
'content-type': 'application/json',
'X-Intellisol-Key-Id': keyId,
'X-Intellisol-Timestamp': timestamp,
'X-Intellisol-Nonce': nonce,
Authorization: `Signature keyId="${keyId}",algorithm="hmac-sha256",signature="${signature}"`,
};
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
return fetch(`https://${host}${uri}`, { method, headers, body: body || undefined });
}

Python​

import hashlib, hmac, json, secrets, time
import requests

def idm_request(host, method, uri, body_obj=None, *, key_id, secret, idempotency_key=None):
body = b"" if body_obj is None else json.dumps(body_obj, separators=(",", ":")).encode()
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
digest = hashlib.sha256(body).hexdigest()
canonical = "\n".join([method.upper(), uri, digest, timestamp, nonce])
signature = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"content-type": "application/json",
"X-Intellisol-Key-Id": key_id,
"X-Intellisol-Timestamp": timestamp,
"X-Intellisol-Nonce": nonce,
"Authorization": f'Signature keyId="{key_id}",algorithm="hmac-sha256",signature="{signature}"',
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
return requests.request(method, f"https://{host}{uri}", data=body or None, headers=headers, timeout=10)

Go​

func sign(req *http.Request, body []byte, keyID, secret string) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
nb := make([]byte, 16)
_, _ = rand.Read(nb)
nonce := hex.EncodeToString(nb)
digest := sha256.Sum256(body)
canonical := strings.Join([]string{
strings.ToUpper(req.Method), req.URL.RequestURI(), hex.EncodeToString(digest[:]), ts, nonce,
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
sig := hex.EncodeToString(mac.Sum(nil))

req.Header.Set("X-Intellisol-Key-Id", keyID)
req.Header.Set("X-Intellisol-Timestamp", ts)
req.Header.Set("X-Intellisol-Nonce", nonce)
req.Header.Set("Authorization", fmt.Sprintf(`Signature keyId=%q,algorithm="hmac-sha256",signature=%q`, keyID, sig))
}

Shell (curl and openssl)​

BODY='{"latitude":5.6037,"longitude":-0.1870}'
URI='/v1/geography/resolve'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
DIGEST=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$URI" "$DIGEST" "$TS" "$NONCE" \
| openssl dgst -sha256 -hmac "$IDM_SECRET" -hex | sed 's/^.* //')

curl -sS "https://$IDM_HOST$URI" -X POST \
-H 'content-type: application/json' \
-H "X-Intellisol-Key-Id: $IDM_KEY_ID" -H "X-Intellisol-Timestamp: $TS" -H "X-Intellisol-Nonce: $NONCE" \
-H "Authorization: Signature keyId=\"$IDM_KEY_ID\",algorithm=\"hmac-sha256\",signature=\"$SIG\"" \
--data-raw "$BODY"

When a signature is refused​

The answer is 401 SIGNATURE_INVALID, and detail.reason tells you which part failed.

detail.reasonCauseFix
timestampYour clock is too far from IDM's. The allowed window is set by Intellisol (five minutes at launch).Sync your servers with NTP. Send seconds, not milliseconds.
nonceThe nonce was seen before inside the window, or is malformed.Generate a new random nonce for every request, including retries.
signatureThe canonical string differs.Check the method is upper case, the URI includes the query string exactly as sent, and the digest is of the exact bytes you sent. Compare against the test vector above.

401 INVALID_API_CREDENTIAL means the key itself is unknown, revoked or expired, or belongs to the other environment. Do not retry it.

Rotating and revoking​

  • Ask Intellisol (or your tenant administrator in IDM Admin) for a new credential before you retire the old one. Both work during the overlap window, so you can deploy without downtime.
  • Revocation is immediate. There is no grace period once a key is revoked, and a credential.revoked webhook is sent to every endpoint you have.
  • A key used from a browser is revoked automatically. See Get started.