Request signing — HMAC-SHA256 v3
The formula is the same for XML and JSON; only the timestamp format and the way the fields are passed differ.
inner = HMAC_SHA256(secret, timestamp)
signature = lowercase( HMAC_SHA256(secret, login + inner) )| Protocol | timestamp | Where it is passed |
|---|---|---|
| XML | yyyyMMddHHmmss UTC | <Header><Authentication>: Login, NowUtc, Signature |
| JSON | ISO-8601 UTC (2026-06-25T12:00:00Z) | Headers X-Login, X-Timestamp, X-Signature, X-Signature-Version: 3 |
Allowed time drift is ±20 minutes. Exceeding the limit → XML ResultCode=101 / JSON HTTP 401.
Step-by-step example
The most common cause of a 401 is a signature computed “almost right”. Check your implementation against this example: it uses fixed inputs, so you must get exactly the same two hex strings.
- Take the fixed inputs:
secret = "password",login = "login",timestamp = "2025-02-10T17:56:37Z". - Compute the intermediate hash
inner— HMAC-SHA256 where the key is the secret and the message is the timestamp:615a772b164395682438043ed16a4de88cae293dde5eb3b37d6f60701e8762ec. - Concatenate the strings:
login + inner, i.e.login615a772b…8762ec. In this order and with no separator. - Compute the signature — HMAC-SHA256 again with the same secret as the key, the message being the result of step 3:
5f9e4f4a7a805b898cac96a6c3e7a81dd0e06e200c291d51663e6f250b10853f.
Common mistakes: signing login + timestamp instead of two HMAC passes; using something other than the secret as the key on the second pass; concatenating inner + login instead of login + inner; passing inner in upper case or as raw bytes instead of a hex string; signing one timestamp value and then putting a freshly recomputed one into the request.
Computing the signature
Sandbox
curl
# bash + openssl
SECRET=demo_password
LOGIN=demo_login
NOW=$(date -u +%Y%m%d%H%M%S) # XML
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) # JSON
INNER=$(printf '%s' "$NOW" \
| openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
SIG=$(printf '%s%s' "$LOGIN" "$INNER" \
| openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
echo "$SIG"Test vectors
Run the vectors before the first network call: two hex strings that do not match are debugged locally in a minute, a 401 from the server is not. If any vector fails, do not send the request: compare your inner and the concatenated message with the printed ones — the difference will be in the timestamp format, the concatenation order, or the hex case.
The full set is at callisto_sigtest.json , including negative vectors: values the server rejects; an implementation that “accepts” them is wrong. The vectors use the public sandbox credentials, so after the local check the same values work in live requests. The request body is not part of the signature — the v3-body-not-signed vector proves it: the signature is identical with and without a body, and Cyrillic in description changes nothing.
Sandbox
{ "id": "v3-json-valid",
"login": "demo_login", "secret": "demo_password",
"timestamp": "2026-02-10T17:56:37Z",
"inner": "ab4ea3fdd8fecb8dd57f19e087fcab319eb344629b0e8502622fcf230d09a6ee",
"outer_message": "demo_loginab4ea3fdd8fecb8dd57f19e087fcab319eb344629b0e8502622fcf230d09a6ee",
"signature": "0c902262f3cfc2282b05a2ccf38f59cce766617badf15c458eb32547f0a0841e" }
{ "id": "v3-xml-valid",
"login": "demo_login", "secret": "demo_password",
"timestamp": "20260210175637",
"inner": "6d722ac72da42f825167f61268f6dbdf0246c18f97889b71da844293367dc5f6",
"signature": "2f412b1a3b08a2ae3a5a427753bff4fd2d719e911d1eaa057d2cfa9e598dc163" }
{ "id": "v3-negative-single-pass", "valid": false,
"why": "single HMAC pass over login+timestamp — the server replies 401",
"signature": "c5261f39339d743804df72d2141653e8f0dc718cd5033f4e04899fed45be067b" }