Skip to main content

Authentication

Every request to the Crypto Payment Gateway public API is authenticated with HMAC-SHA256 over a canonical request string. This page walks you through obtaining a key, signing your first request, and verifying the round-trip with the no-op GET /v1/_ping endpoint.

1. Get an API key

  1. Sign in to your dashboard.
  2. Open the API Keys page.
  3. Click Create key, give it a label, pick the permissions it needs, and (optionally) restrict it to a list of source IPs.
  4. The dashboard shows the secret exactly once. Copy it into your secrets store now — it isn't retrievable later. If you lose it, revoke the key and create a new one.

Each key has:

  • key_id — a public identifier you send in the X-CPG-Key header.
  • secret — the signing secret. Never log it, never put it in client-side code, never check it into git.
  • Permissions — a subset of request_deposit_address, request_withdrawal_funding_address, withdraw, read_balance, read_deposit_status, read_withdrawal_status, list_transactions, list_currencies. Endpoints reject keys that don't carry the required permission with 403.
  • Rate limit and optional IP whitelist — see Rate limits & restrictions.

2. The three required headers

Every authenticated request must carry these three headers:

HeaderValue
X-CPG-KeyYour key's key_id.
X-CPG-TimestampCurrent Unix time in seconds, as an integer string (e.g. 1747444800).
X-CPG-SignatureLowercase hex of HMAC-SHA256(secret, canonical_string) — see below.

If any of the three is missing, the server replies with 401 Missing authentication headers before processing anything else.

3. Building the canonical string

The string you sign is exactly the following five fields joined by single \n characters, with no trailing newline:

{X-CPG-Timestamp}\n
{HTTP_METHOD_UPPERCASE}\n
{request_path}\n
{query_string}\n
{sha256_hex_of_raw_body}

Concrete details that trip people up:

  • HTTP_METHOD_UPPERCASEGET, POST, DELETE, etc. Always uppercase.
  • request_path — starts with /, no host, no query. For GET https://api/v1/_ping the path is /v1/_ping.
  • query_string — everything after ? in the URL (without the ?), in the exact order you send it. For requests with no query, this is the empty string (not absent — the \n separator still has to be there).
  • sha256_hex_of_raw_body — SHA-256 of the raw body bytes you'll send, as lowercase hex. For requests with no body (every GET), hash the empty byte string b"" — its hex digest is the well-known e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Don't omit this field; the \n separator stays.
  • No trailing newline after the body hash. The string ends with the last hex character.

The server recomputes this exact string from your request and compares signatures with hmac.compare_digest. Any single byte off and you get 401 Invalid signature.

4. Signing your first request

The recipes below all hit GET /v1/_ping, the gateway's "are my credentials wired correctly?" endpoint. It requires the read_balance permission and returns:

{
"success": true,
"message": "pong",
"data": { "key_id": "your_key_id_here" }
}

Replace BASE_URL, API_KEY, and API_SECRET with your values.

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://your-api.example.com"
API_KEY="your_key_id_here"
API_SECRET="your_secret_here"

METHOD="GET"
ENDPOINT="/v1/_ping"
QUERY=""
BODY=""

TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" "$METHOD" "$ENDPOINT" "$QUERY" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | awk '{print $2}')

curl -sS "${BASE_URL}${ENDPOINT}" \
-H "X-CPG-Key: ${API_KEY}" \
-H "X-CPG-Timestamp: ${TS}" \
-H "X-CPG-Signature: ${SIG}"

Pure curl can't compute the signature alone — every shipped example uses openssl to hash and HMAC. Save this as ping.sh, chmod +x ping.sh, and run it.

If the response prints {"success": true, "message": "pong", ...}, your credentials and signing logic are correct and you're ready to call any other endpoint in the same way.

5. Common signing mistakes

The vast majority of 401 Invalid signature errors come down to one of these. Check each before suspecting a real bug.

  1. Forgot to hash the empty body on GETs. The body-hash field has to be present even when there's no body. Hash b"" (the empty byte string) — that's e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Omitting the field entirely (or sending an empty body hash field) breaks the canonical string.
  2. Wrong query-string format. Pass the query string as you'll send it, in the same key order, with no leading ?. If you reorder keys client-side before signing but the HTTP client reorders them again before sending, signatures won't match.
  3. Mixed case method. Use GET, not Get or get. The middleware uppercases internally; sign with the uppercased value.
  4. Path includes the host. The path is just /v1/.... Including https://api.example.com or the host in the path field will not match what the server reconstructs.
  5. Clock skew. If your machine's clock is more than 300 seconds off NTP, every request fails as out-of-window. Run ntpd / chrony / systemd-timesyncd.
  6. HTTP client mangling the headers. The API treats header names case-insensitively and compares signatures case-insensitively, so casing is not the worry — but some HTTP clients/middleware strip or merge custom headers entirely. If you're getting 401 Missing authentication headers, dump the actual outbound request and confirm all three X-CPG-* headers are present on the wire.
  7. Re-serializing the body. If you build a JSON string in your code, sign that exact byte string, and then let your HTTP client re-serialize a parsed dict (changing key order or whitespace), the signature won't match. Always sign the bytes you'll actually send.

When in doubt, log the canonical string locally and compare to what you'd expect server-side.

Next steps

  • Read Errors & response envelope for the full error catalogue and the shape of every successful response.
  • Read Rate limits & restrictions for per-key limits, replay protection, and IP whitelist behaviour.
  • Then jump to the Reference section (coming in step 14.3) to start calling real endpoints.