Skip to content

HMAC Authentication

This page walks through the complete HMAC-SHA256 signature computation process for authenticating API requests to SlaunchX. Every request to /api/** endpoints must be signed using this procedure.

Required Headers

Every API request must include these headers:

HeaderRequiredDescription
X-Api-KeyYesYour API key identifier (e.g., sk_live_abc123)
X-TimestampYesCurrent Unix epoch in seconds (UTC). Must be within 60 seconds of server time
X-NonceYesUnique request identifier. Use UUID v4 to guarantee uniqueness. Each nonce can only be used once within the timestamp window
X-SignatureYesHMAC-SHA256 signature of the canonical message, Base64-encoded. See Signature Computation below
X-LOCALENoPreferred response language (e.g., en-US, zh-CN). Affects localized error messages
Content-TypeConditionalapplication/json — required for requests with a body (POST, PUT, PATCH)

Signature Computation

The signature is computed in three steps: hash the request body, build a canonical message string, and compute the HMAC.

Step 1: Compute the Body Hash

Compute the SHA-256 hash of the raw request body and encode it as a lowercase hexadecimal string.

bodyHash = lowercase_hex(SHA-256(requestBody))

For requests with a body (POST, PUT, PATCH), hash the exact JSON string you will send:

bash
echo -n '{"sourceWalletId":"w_123","amount":"100.00"}' | openssl dgst -sha256 -hex
# Output: a1fce4363854ff888cff4b8e7875d600c...

For requests without a body (GET, DELETE), use the SHA-256 hash of an empty string:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

:::caution Body encoding matters The body hash is computed over the exact byte sequence sent in the HTTP body. If your JSON serializer produces {"a": 1} (with a space after the colon) but you hash {"a":1} (without the space), the signature will not match. Always hash the same string you pass to your HTTP client. :::

Step 2: Build the Canonical Message

Concatenate five components separated by newline characters (\n):

canonicalMessage = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash
ComponentDescriptionExample
methodHTTP method, uppercasePOST
pathRequest path only -- no scheme, host, or query string/api/v1/transfers
timestampUnix epoch seconds (UTC), same value as the X-Timestamp header1709337600
nonceUnique identifier, same value as the X-Nonce header (UUID v4 recommended)550e8400-e29b-41d4-a716-446655440000
bodyHashSHA-256 hex digest from Step 1a1fce436...

For a POST /api/v1/transfers request, the canonical message would look like:

POST
/api/v1/transfers
1709337600
550e8400-e29b-41d4-a716-446655440000
a1fce4363854ff888cff4b8e7875d600c...

:::caution Path must not include query string For GET requests with query parameters (e.g., /api/v1/wallets?page=0&size=20), only include the path portion in the canonical message: /api/v1/wallets. Query parameters are excluded from signature computation. :::

Step 3: Compute the HMAC Signature

Sign the canonical message using HMAC-SHA256 with your API secret, then Base64-encode the result:

signature = Base64(HMAC-SHA256(canonicalMessage, apiSecret))

The resulting Base64 string goes into the X-Signature header.


Complete Examples

Bash + curl

The following script demonstrates a complete signed API request:

bash
#!/bin/bash
# SlaunchX API Integration -- HMAC-SHA256 Authentication Example

# ─── Configuration ────────────────────────────────────────────
API_KEY="sk_live_abc123def456"
API_SECRET="your-api-secret-here"
BASE_URL="https://api.slaunchx.cc"  # Brand-specific: replace with your API host

# ─── Request parameters ──────────────────────────────────────
METHOD="POST"
PATH_URI="/api/v1/transfer/command/create"
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')

# Request body (must be the exact string sent over the wire)
BODY='{"sourceWalletId":"w_123","targetWalletId":"w_456","amount":"100.00","currency":"USD"}'

# ─── Step 1: Body hash ───────────────────────────────────────
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')

# ─── Step 2: Canonical message ────────────────────────────────
CANONICAL="${METHOD}\n${PATH_URI}\n${TIMESTAMP}\n${NONCE}\n${BODY_HASH}"

# ─── Step 3: HMAC-SHA256 signature ────────────────────────────
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | openssl base64 -A)

# ─── Send the request ─────────────────────────────────────────
curl -X "$METHOD" "${BASE_URL}${PATH_URI}" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Signature: $SIGNATURE" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Nonce: $NONCE" \
  -d "$BODY"

For GET requests, omit the -d flag and use the empty-string body hash:

bash
METHOD="GET"
PATH_URI="/api/v1/wallets"
BODY=""
BODY_HASH="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

CANONICAL="${METHOD}\n${PATH_URI}\n${TIMESTAMP}\n${NONCE}\n${BODY_HASH}"
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | openssl base64 -A)

curl -X "$METHOD" "${BASE_URL}${PATH_URI}" \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Signature: $SIGNATURE" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Nonce: $NONCE"

Node.js

A reusable function for signing and sending API requests:

javascript
import crypto from 'crypto';

/**
 * Send an authenticated request to the SlaunchX API.
 *
 * @param {string} method   - HTTP method (GET, POST, PUT, DELETE)
 * @param {string} path     - Request path (e.g., /api/v1/wallets)
 * @param {object|null} body - Request body (null for GET/DELETE)
 * @param {string} apiKey   - Your API key (sk_live_xxx)
 * @param {string} apiSecret - Your API secret
 * @returns {Promise<object>} Parsed JSON response
 */
async function apiRequest(method, path, body, apiKey, apiSecret) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomUUID();

  // Step 1: Body hash
  const bodyStr = body ? JSON.stringify(body) : '';
  const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('hex');

  // Step 2: Canonical message
  const canonical = [method, path, timestamp, nonce, bodyHash].join('\n');

  // Step 3: HMAC-SHA256 signature
  const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(canonical)
    .digest('base64');

  // Send request
  // Send request (replace api.slaunchx.cc with your API host)
  const res = await fetch(`https://api.slaunchx.cc${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': apiKey,
      'X-Signature': signature,
      'X-Timestamp': timestamp,
      'X-Nonce': nonce,
    },
    body: bodyStr || undefined,
  });

  return res.json();
}

// ─── Usage examples ──────────────────────────────────────────

// POST: Create a transfer
const transfer = await apiRequest(
  'POST',
  '/api/v1/transfer/command/create',
  {
    sourceWalletId: 'w_123',
    targetWalletId: 'w_456',
    amount: '100.00',
    currency: 'USD',
  },
  'sk_live_abc123def456',
  'your-api-secret-here'
);

// GET: List wallets (no body)
const wallets = await apiRequest(
  'GET',
  '/api/v1/wallets',
  null,
  'sk_live_abc123def456',
  'your-api-secret-here'
);

Server Validation Pipeline

When the server receives an API request, it validates the request through a multi-step pipeline. A failure at any step short-circuits processing and returns the corresponding error code.

┌─────────────────────────────────────────────────────────────────┐
│                       Validation Pipeline                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. API Key Lookup                                              │
│     ├── Missing X-Api-Key header       →  GA2001 (401)          │
│     └── Key not found                  →  GA2011 (401)          │
│                                                                 │
│  2. Key Status Check                                            │
│     └── Key disabled by administrator  →  GA2021 (403)          │
│                                                                 │
│  3. Workspace Resolution                                        │
│     ├── Workspace not found            →  GA2032 (404)          │
│     └── Not a workspace member         →  GA2034 (403)          │
│                                                                 │
│  4. Timestamp Validation                                        │
│     └── |server_time - timestamp| > 60s →  GA2013 (401)         │
│                                                                 │
│  5. Nonce Uniqueness                                            │
│     └── Nonce already used             →  GA2013 (401)          │
│                                                                 │
│  6. HMAC Signature Verification                                 │
│     └── Computed HMAC ≠ X-Signature    →  GA2012 (401)          │
│                                                                 │
│  7. IP Whitelist (optional)                                     │
│     └── Client IP not in whitelist     →  GA2022 (403)          │
│                                                                 │
│  8. Scope Check                                                 │
│     └── Missing required scope         →  GA2024 (403)          │
│                                                                 │
│  ✓  All checks passed → process request                        │
└─────────────────────────────────────────────────────────────────┘

Validation order matters

The pipeline validates in a fixed order. For example, if your API key is valid but your timestamp is stale, you will receive GA2013 (timestamp expired) rather than a signature error. Use the error code to diagnose exactly which step failed.


API Scopes

API keys are workspace-scoped — each key belongs to a single workspace and can only access resources within that workspace. Cross-workspace access is not possible via API keys.

Each API key is assigned one or more scopes that control which endpoints it can access:

ScopeDescription
wallet:readQuery wallets within the workspace
transfer:readQuery transfer records within the workspace
transfer:createCreate transfer orders between wallets in the workspace

Accessible Endpoints

ScopeMethodEndpointDescription
wallet:readGET/api/v1/walletsList wallets
wallet:readGET/api/v1/wallets/{id}Get wallet details
wallet:readGET/api/v1/wallets/{id}/balanceGet wallet balance
wallet:readGET/api/v1/wallets/{id}/flowsList wallet flows
transfer:readGET/api/v1/transfer/query/ordersList transfer orders
transfer:readGET/api/v1/transfer/query/orders/wallet/{walletId}List transfers by wallet
transfer:readGET/api/v1/transfer/query/orders/{bizId}Get transfer order
transfer:readGET/api/v1/transfer/query/orders/{bizId}/completedCheck transfer completion
transfer:createPOST/api/v1/transfer/command/createCreate transfer order

Endpoints without a declared API scope are not accessible via the API chain (deny-by-default). Requests to such endpoints, or requests missing the required scope, will receive a 403 Forbidden response with error code 50090201 (API_ACCESS_DENIED).

Workspace Isolation

All API responses are automatically scoped to the workspace bound to the API key. Transfer query endpoints enforce workspace ownership validation — if a transfer order belongs to a different workspace, the request returns an error. This prevents cross-workspace data leakage even when using valid order IDs.


Example Responses

A successful API response:

Successful transfer200
{
  "version": "1.3.0",
  "timestamp": 1709337600000,
  "success": true,
  "code": "2000",
  "message": "SUCCESS",
  "data": {
    "transferId": "txn_789xyz",
    "status": "COMPLETED",
    "amount": "100.00",
    "currency": "USD"
  }
}

An authentication failure:

Signature verification failed401
{
  "version": "1.3.0",
  "timestamp": 1709337600000,
  "success": false,
  "code": "GA2012",
  "message": "SIGNATURE_INVALID",
  "data": null
}

Common Pitfalls

:::caution Timestamp clock drift The server rejects requests where the timestamp differs from server time by more than 60 seconds. If you see GA2013 errors, check that your server's clock is synchronized via NTP. When debugging locally, compare date +%s on your machine against an NTP server. :::

:::caution Body hash for empty bodies GET and DELETE requests have no body. You must still include a body hash in the canonical message -- use the SHA-256 of an empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Omitting it or using a different value will cause GA2012. :::

:::caution JSON serialization consistency The body hash covers the exact bytes of the request body. If you compute the hash over one JSON string but your HTTP library re-serializes the body (changing key order, spacing, or encoding), the signature will not match. Best practice: serialize once, hash that string, and send that same string as the body. :::

:::caution Nonce reuse Each nonce must be globally unique within the 60-second timestamp window. Reusing a nonce (even with a different request body) results in GA2013. Use UUID v4 to guarantee uniqueness. :::

Next Steps

SlaunchX API Documentation