CryptoLike API — Authentication
Every call to the merchant API (/v1/*, except the public catalogue and the payment page) is
signed with your API key. Signing takes three headers and one HMAC. Once your client signs
correctly, it signs correctly for every endpoint.
1. Keys
Create keys in the cabinet: API keys → Create key. You get:
| Field | Example | Notes |
|---|---|---|
| Key ID | ck_live_4f9k2m7p1q8r3s6t0u5v9w2x |
Public. Sent in X-API-Key. ck_test_… for test keys. |
| Secret | kZ1Yv7pQ2xL9mN4tR8wE3uH6jB5cF0aD1gS2hK7lP9o |
Shown once. 43 characters. Store it in your secret manager. |
Each key has permissions (read, create_invoice, create_address, withdraw), an optional
IP whitelist and an optional request limit. The cabinet shows the last 4 characters of the
secret (…lP9o) so you can tell keys apart; nobody, including us, can display the full secret
again. Lost it? Rotate the key (§8).
2. Headers
| Header | Value |
|---|---|
X-API-Key |
your key ID |
X-Timestamp |
current Unix time in seconds (1758000000) |
X-Signature |
lower-case hex HMAC-SHA256 of the canonical string (§3), keyed with the secret |
Content-Type |
application/json for requests with a body |
Idempotency-Key |
optional, POST only — see §7 |
3. Canonical string
Join four lines with \n (LF, no trailing newline):
<timestamp>
<METHOD>
<path[?query]>
<hex(sha256(body))>timestamp— exactly theX-Timestampvalue.METHOD— upper-case HTTP method (GET,POST).path[?query]— the request target as you send it: path plus query string if any, no scheme or host./v1/invoices,/v1/balances?limit=10. Sign the query the way you encode it.hex(sha256(body))— lower-case hex SHA-256 of the raw request body bytes. For an empty body (everyGET) it ise3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Then X-Signature = hex(HMAC-SHA256(key = secret bytes, message = canonical string)). The key is
the secret string exactly as shown — do not base64-decode it.
Reference vector
Use it to check your implementation before the first real call.
secret kZ1Yv7pQ2xL9mN4tR8wE3uH6jB5cF0aD1gS2hK7lP9o
timestamp 1758000000
method POST
path /v1/invoices
body {"amount":"10.50","currency":"USD","order_id":"A-1001"}
sha256(body) 50bf8f3c2622754522fb1343f2597a4843d08e4649255eefbdce1f800d3b457f
canonical string "1758000000\nPOST\n/v1/invoices\n50bf8f3c2622754522fb1343f2597a4843d08e4649255eefbdce1f800d3b457f"
X-Signature 7878d965e6eb95b80df12e34e9466d40a8e12aa612653f72760f5dc5c06323a1GET /v1/balances?limit=10 with the same secret and timestamp, empty body →
34b530cc1f7d5760f991e23d1f8c499bb166356d7da7c17f7984016199be9310.
4. Examples
curl
KEY_ID=ck_live_…; SECRET=…
TS=$(date +%s); METHOD=POST; PATH_Q=/v1/invoices
BODY='{"amount":"10.50","currency":"USD","order_id":"A-1001"}'
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATH_Q" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -sS "https://api.cryptolike.net$PATH_Q" -X "$METHOD" \
-H "X-API-Key: $KEY_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
-H "Content-Type: application/json" -H "Idempotency-Key: order-A-1001" \
--data "$BODY"Node.js
import crypto from "node:crypto";
export async function call(method, pathWithQuery, body = "") {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const canonical = [ts, method.toUpperCase(), pathWithQuery, bodyHash].join("\n");
const sig = crypto.createHmac("sha256", process.env.CL_SECRET).update(canonical).digest("hex");
return fetch("https://api.cryptolike.net" + pathWithQuery, {
method,
headers: {
"X-API-Key": process.env.CL_KEY_ID,
"X-Timestamp": ts,
"X-Signature": sig,
"Content-Type": "application/json",
},
body: body || undefined,
});
}
// await call("POST", "/v1/invoices", JSON.stringify({ amount: "10.50", currency: "USD", order_id: "A-1001" }));Python
import hashlib, hmac, os, time, requests
def call(method: str, path_with_query: str, body: bytes = b"") -> requests.Response:
ts = str(int(time.time()))
body_hash = hashlib.sha256(body).hexdigest()
canonical = "\n".join([ts, method.upper(), path_with_query, body_hash])
sig = hmac.new(os.environ["CL_SECRET"].encode(), canonical.encode(), hashlib.sha256).hexdigest()
return requests.request(
method, "https://api.cryptolike.net" + path_with_query, data=body or None,
headers={"X-API-Key": os.environ["CL_KEY_ID"], "X-Timestamp": ts,
"X-Signature": sig, "Content-Type": "application/json"},
)
# call("GET", "/v1/balances")Go
func sign(secret string, ts int64, method, pathQuery string, body []byte) string {
sum := sha256.Sum256(body)
canonical := strconv.FormatInt(ts, 10) + "\n" + strings.ToUpper(method) + "\n" + pathQuery + "\n" + hex.EncodeToString(sum[:])
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
ts := time.Now().Unix()
req, _ := http.NewRequest("POST", "https://api.cryptolike.net/v1/invoices", bytes.NewReader(body))
req.Header.Set("X-API-Key", keyID)
req.Header.Set("X-Timestamp", strconv.FormatInt(ts, 10))
req.Header.Set("X-Signature", sign(secret, ts, "POST", "/v1/invoices", body))
req.Header.Set("Content-Type", "application/json")5. Errors
All errors share one shape: {"error":{"code":"…","message":"…","details":{…}}}. Branch on
code; message is for humans and may change.
| HTTP | code |
Meaning | What to do |
|---|---|---|---|
| 401 | auth.missing_header |
One of the three headers is absent. | Send all three. |
| 401 | auth.invalid_key |
Unknown, disabled or revoked key. | Check the key ID; enable or create a key in the cabinet. |
| 401 | auth.invalid_timestamp |
X-Timestamp is not an integer. |
Send Unix seconds. |
| 401 | auth.expired_timestamp |
` | server time − timestamp |
| 401 | auth.invalid_signature |
HMAC mismatch. | Re-check the canonical string against the vector: method case, query string, body bytes, secret as shown. |
| 401 | auth.replay |
This exact signature was already accepted. | Each request needs a fresh timestamp; do not resend a captured request. |
| 403 | auth.ip_not_allowed |
Client IP is outside the key's whitelist. details.ip shows the address we saw. |
Add the IP/CIDR in the cabinet or use a key without a whitelist. |
| 403 | auth.permission_denied |
Key lacks the scope in details.required. |
Grant the permission or use another key. |
| 429 | rate_limited |
Key over its request limit. Retry-After in seconds. |
Back off; see §6. |
| 409 | idempotency.in_progress |
The same Idempotency-Key is being processed now. |
Wait and repeat the same request. |
| 422 | idempotency.mismatch |
The same Idempotency-Key was used with a different request. |
Use one key per logical operation. |
| 413 | payload_too_large |
Body over 1 MiB. | — |
Every 401 also carries WWW-Authenticate: CryptoLike-HMAC-SHA256.
6. Request limits
Each key has a token bucket: X-RateLimit-Limit requests per minute (default 600), refilled
continuously; X-RateLimit-Remaining tells you how many are left right now. Over the limit:
429 rate_limited with Retry-After. Unauthenticated requests (wrong signature, expired
timestamp) never consume your key's tokens. A per-key limit can be set in the cabinet.
7. Idempotency (POST)
Send Idempotency-Key: <your unique value> (1–128 printable ASCII characters, e.g. your
order ID) with any POST. If the request is repeated — a timeout, a retry, a duplicate job —
you get the original response back (Idempotent-Replayed: true) and nothing is created twice.
Keys are remembered for 24 hours per API key. The repeat must be the same request: same
method, path and body; a different body with the same key is 422 idempotency.mismatch.
Only success responses (2xx) are stored. An error response (4xx — nothing was created — or
5xx) releases the key, so a retry after our error, after you fix the key's permissions, switch a
coin on or correct the request, runs again and reflects the current state.
8. Rotation and revocation
- Rotate (cabinet → key → Rotate) issues a new secret and shows it once. The previous
secret keeps working for 10 minutes so you can deploy the new one without downtime;
previous_secret_valid_untilin the key tells you when it stops. Rotating again within that window drops the oldest secret — at most two secrets verify at any time. - Disable stops the key until you enable it again (
401 auth.invalid_keymeanwhile). - Revoke is final: the key stops immediately and cannot be re-enabled. Create a new one.
Every creation, change, rotation and revocation is written to the team security log with the person who did it.
9. Test keys
ck_test_… keys sign exactly like live keys. They are meant for integration environments;
the test-mode behaviour of each endpoint (sandbox payments) is described with that endpoint.
10. Checklist when a signature fails
- Timestamp in seconds, not milliseconds; clock synced.
- Method upper-case; path includes the query string exactly as sent.
- Body hash over the exact bytes you send (mind pretty-printing and trailing newlines).
- HMAC key is the secret string itself, not decoded.
- Signature is hex, 64 characters.
- The four parts are joined with
\nand nothing after the last one.