Skip to content

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):

text
<timestamp>
<METHOD>
<path[?query]>
<hex(sha256(body))>
  • timestamp — exactly the X-Timestamp value.
  • 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 (every GET) it is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

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.

text
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       7878d965e6eb95b80df12e34e9466d40a8e12aa612653f72760f5dc5c06323a1

GET /v1/balances?limit=10 with the same secret and timestamp, empty body → 34b530cc1f7d5760f991e23d1f8c499bb166356d7da7c17f7984016199be9310.

4. Examples

curl

bash
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

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

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

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_until in 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_key meanwhile).
  • 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

  1. Timestamp in seconds, not milliseconds; clock synced.
  2. Method upper-case; path includes the query string exactly as sent.
  3. Body hash over the exact bytes you send (mind pretty-printing and trailing newlines).
  4. HMAC key is the secret string itself, not decoded.
  5. Signature is hex, 64 characters.
  6. The four parts are joined with \n and nothing after the last one.