# decorate API # API reference Build your storefront with decorate. Base URL: https://api.decorate.gg/v1 Browse the live catalogue, quote a cart, and place orders from your decorate balance. Gift links appear on completed orders and can be delivered to your server through signed webhooks. The API uses versioned REST endpoints and JSON. Every amount is an integer in USD cents. A price of 250 means $2.50. Your API orders use the same store prices and fulfillment process as the shop. Sign in with Discord to create a scoped key. Keep its secret on your server, add funds to your balance, and make your first signed request. Note: Requests can place real orders and spend real balance. There is no sandbox environment. Note: Your API secret belongs on your server. Never include it in browser code or a public repository. ## Node.js signing helper ```js import { createHash, createHmac, randomBytes } from 'node:crypto'; const BASE = 'https://api.decorate.gg'; const KEY_ID = process.env.DECORATE_KEY_ID; const SECRET = process.env.DECORATE_API_SECRET; async function call(method, path, payload) { const url = new URL(path, BASE); const body = payload === undefined ? '' : JSON.stringify(payload); const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = randomBytes(16).toString('hex'); const hash = createHash('sha256').update(body).digest('hex'); const canonical = [method.toUpperCase(), url.pathname + url.search, timestamp, nonce, hash].join('\n'); const signature = createHmac('sha256', SECRET) .update(canonical).digest('hex'); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': timestamp, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature, }, ...(body ? { body } : {}), signal: AbortSignal.timeout(30000), }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); return data; } console.log(await call('GET', '/v1/ping')); ``` ## Python signing helper ```python import hashlib, hmac, json, os, secrets, time from urllib.request import Request, urlopen from urllib.parse import urlsplit BASE = 'https://api.decorate.gg' KEY_ID = os.environ['DECORATE_KEY_ID'] SECRET = os.environ['DECORATE_API_SECRET'] def call(method, path, payload=None): body = (json.dumps(payload, separators=(',', ':'), ensure_ascii=False) if payload is not None else '') raw = body.encode('utf-8') ts = str(int(time.time())) nonce = secrets.token_hex(16) url = BASE + path parts = urlsplit(url) target = parts.path + ('?' + parts.query if parts.query else '') canonical = '\n'.join([method.upper(), target, ts, nonce, hashlib.sha256(raw).hexdigest()]) signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest() req = Request(url, data=raw if raw else None, method=method, headers={'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': ts, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature}) with urlopen(req, timeout=30) as response: return json.load(response) print(call('GET', '/v1/ping')) ``` --- # Your first request From a new key to a signed request. Base URL: https://api.decorate.gg/v1 Create an API key in your dashboard. Choose only the permissions your integration needs and save the secret when it is shown. You will not be able to read it again. Set DECORATE_KEY_ID and DECORATE_API_SECRET in your server environment. Copy the Node.js or Python helper, then call GET /v1/ping to check your credentials. Fetch the catalogue, quote the items you want, then add funds before placing an order. Use a unique idempotency key for each purchase; keep it when retrying that same purchase. ## Node.js signing helper ```js import { createHash, createHmac, randomBytes } from 'node:crypto'; const BASE = 'https://api.decorate.gg'; const KEY_ID = process.env.DECORATE_KEY_ID; const SECRET = process.env.DECORATE_API_SECRET; async function call(method, path, payload) { const url = new URL(path, BASE); const body = payload === undefined ? '' : JSON.stringify(payload); const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = randomBytes(16).toString('hex'); const hash = createHash('sha256').update(body).digest('hex'); const canonical = [method.toUpperCase(), url.pathname + url.search, timestamp, nonce, hash].join('\n'); const signature = createHmac('sha256', SECRET) .update(canonical).digest('hex'); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': timestamp, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature, }, ...(body ? { body } : {}), signal: AbortSignal.timeout(30000), }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); return data; } console.log(await call('GET', '/v1/ping')); ``` ## Python signing helper ```python import hashlib, hmac, json, os, secrets, time from urllib.request import Request, urlopen from urllib.parse import urlsplit BASE = 'https://api.decorate.gg' KEY_ID = os.environ['DECORATE_KEY_ID'] SECRET = os.environ['DECORATE_API_SECRET'] def call(method, path, payload=None): body = (json.dumps(payload, separators=(',', ':'), ensure_ascii=False) if payload is not None else '') raw = body.encode('utf-8') ts = str(int(time.time())) nonce = secrets.token_hex(16) url = BASE + path parts = urlsplit(url) target = parts.path + ('?' + parts.query if parts.query else '') canonical = '\n'.join([method.upper(), target, ts, nonce, hashlib.sha256(raw).hexdigest()]) signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest() req = Request(url, data=raw if raw else None, method=method, headers={'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': ts, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature}) with urlopen(req, timeout=30) as response: return json.load(response) print(call('GET', '/v1/ping')) ``` --- # Authentication Signed requests. Scoped access. Replay protection. Base URL: https://api.decorate.gg/v1 Every endpoint except service status requires HMAC-SHA256 authentication. Sign the method, the exact path and query string, timestamp, nonce, and SHA-256 hash of the raw body, separated by newline characters. Send a fresh cryptographically random nonce for every attempt. A retried order needs a new nonce and timestamp, but the same idempotency key and body. Sign the exact bytes you send; do not serialize the body again afterward. ## Parameters - `X-Api-Key` (string, required): The public ID of your API key. - `X-Api-Timestamp` (integer, required): Current Unix time in seconds, within 60 seconds of our clock. Keep your server clock synchronized. - `X-Api-Nonce` (string, required): A fresh 32–64 character lowercase hexadecimal value. The helper generates 16 random bytes as hex. - `X-Api-Signature` (string, required): Lowercase hexadecimal HMAC-SHA256 of the canonical request, using your API secret. Note: An empty body still has a SHA-256 hash. The helpers handle GET requests correctly. Note: Key scopes restrict what an integration can do. Revoking a key stops authentication immediately. Note: Limits: 120 authenticated requests per minute per account, 20 order attempts per minute, and 16 KiB request bodies. The complete body must arrive within 5 seconds. ## Node.js signing helper ```js import { createHash, createHmac, randomBytes } from 'node:crypto'; const BASE = 'https://api.decorate.gg'; const KEY_ID = process.env.DECORATE_KEY_ID; const SECRET = process.env.DECORATE_API_SECRET; async function call(method, path, payload) { const url = new URL(path, BASE); const body = payload === undefined ? '' : JSON.stringify(payload); const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = randomBytes(16).toString('hex'); const hash = createHash('sha256').update(body).digest('hex'); const canonical = [method.toUpperCase(), url.pathname + url.search, timestamp, nonce, hash].join('\n'); const signature = createHmac('sha256', SECRET) .update(canonical).digest('hex'); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': timestamp, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature, }, ...(body ? { body } : {}), signal: AbortSignal.timeout(30000), }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); return data; } console.log(await call('GET', '/v1/ping')); ``` ## Python signing helper ```python import hashlib, hmac, json, os, secrets, time from urllib.request import Request, urlopen from urllib.parse import urlsplit BASE = 'https://api.decorate.gg' KEY_ID = os.environ['DECORATE_KEY_ID'] SECRET = os.environ['DECORATE_API_SECRET'] def call(method, path, payload=None): body = (json.dumps(payload, separators=(',', ':'), ensure_ascii=False) if payload is not None else '') raw = body.encode('utf-8') ts = str(int(time.time())) nonce = secrets.token_hex(16) url = BASE + path parts = urlsplit(url) target = parts.path + ('?' + parts.query if parts.query else '') canonical = '\n'.join([method.upper(), target, ts, nonce, hashlib.sha256(raw).hexdigest()]) signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest() req = Request(url, data=raw if raw else None, method=method, headers={'Content-Type': 'application/json', 'X-Api-Key': KEY_ID, 'X-Api-Timestamp': ts, 'X-Api-Nonce': nonce, 'X-Api-Signature': signature}) with urlopen(req, timeout=30) as response: return json.load(response) print(call('GET', '/v1/ping')) ``` --- # Errors & retries Handle the status code and the machine-readable error. Base URL: https://api.decorate.gg/v1 Errors return an error object with a stable type and a readable message. Log the request ID, status, and error type to investigate a failed call. Do not log API secrets or signatures. Retry network failures and rate limits with backoff. When retrying an order, keep its original idempotency key and exact cart. A new idempotency key represents a new purchase. ## Parameters - `400` (Invalid request): Check the required fields, item quantities, and parameter formats. - `401` (Authentication failed): Check your key, signature, timestamp, and nonce. - `403` (Access denied): The key lacks a required scope or the account is suspended. - `404` (Not found): The item or order does not exist, or the order is not yours. - `409` (Conflict): Re-quote a changed price or unavailable cart. Reusing an idempotency key with a different request is rejected. - `402` (Insufficient balance): Add funds before retrying the order. - `429` (Rate limited): Wait for the Retry-After interval before making another request. - `408` (Request timed out): The request body did not finish within 5 seconds. Retry with a fresh timestamp and nonce. - `413` (Body too large): Keep the complete UTF-8 request body at or below 16 KiB. - `503` (Store unavailable): The store is temporarily closed. Check service status before retrying an order. ## Example response ```json { "error": { "type": "insufficient_funds", "message": "Your balance is too low for this order." } } ``` --- # Idempotent orders Retry a purchase without buying it twice. Base URL: https://api.decorate.gg/v1 Every order requires an idempotency_key. Generate it once in your system and save it with your customer’s purchase. It is scoped to your account, so it remains usable if you rotate your API key. Repeating the same key and request returns the original order without charging again. Reusing it with different items is a conflict. Concurrent requests are protected by the same transactional check. Orders are all or nothing. If any item is unavailable, the price has changed, or the balance is insufficient, no partial cart is purchased. ## Request body ```json { "idempotency_key": "your-order-1042", "items": [ { "id": "ITEM_ID", "qty": 1, "expected_price_cents": 250 } ] } ``` --- # Service status Check API availability without authenticating. Base URL: https://api.decorate.gg/v1 `GET /v1/status` Required scope: None (public) ## Example response ```json { "status": "operational", "api": "up", "store": "open", "queue": 2, "updated": "2026-10-15T12:00:00Z" } ``` Note: A successful status response confirms this endpoint is available. It is not an estimated delivery time. --- # Check authentication Make a small signed call to verify your integration. Base URL: https://api.decorate.gg/v1 `GET /v1/ping` Required scope: account:read ## Example response ```json { "ok": true, "name": "My storefront", "status": "active", "account_status": "active" } ``` Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Retrieve balance Read the balance available for API orders. Base URL: https://api.decorate.gg/v1 `GET /v1/balance` Required scope: account:read ## Example response ```json { "balance_cents": 4200, "currency": "USD" } ``` Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # List items Live store prices, availability, and collectible previews. Base URL: https://api.decorate.gg/v1 `GET /v1/catalogue` Required scope: catalogue:read ## Parameters - `search` (string): Filter by item name. - `category` (string): Filter by collection name (case-insensitive contains). - `type` (string): One of decoration, effect, nameplate, frame, or bundle. - `limit` (integer): 1–2,000 items; defaults to 100. - `offset` (integer): Number of matching items to skip. ## Example response ```json { "items": [ { "id": "ITEM_ID", "name": "Example decoration", "type": "decoration", "collection": "Example collection", "price_cents": 250, "currency": "USD", "image": "https://cdn.discordapp.com/media/v1/collectibles-shop/ITEM_ID/static", "stock": null, "sellable": true, "available_until": null } ], "count": 1, "total": 1, "limit": 100, "offset": 0 } ``` Note: Example IDs in these docs are placeholders. Use an actual item ID returned by the catalogue. Note: Check sellable before offering an item. The catalogue includes unavailable items. Stock is null because availability is not represented as a unit count. Note: Images are absolute URLs when a single preview exists. Frames use composited layers, so their image is null. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Retrieve an item Check one item before adding it to your cart. Base URL: https://api.decorate.gg/v1 `GET /v1/catalogue/{id}` Required scope: catalogue:read ## Parameters - `id` (string · path, required): An item ID from the catalogue. ## Example response ```json { "id": "ITEM_ID", "name": "Example decoration", "type": "decoration", "collection": "Example collection", "price_cents": 250, "currency": "USD", "image": "https://cdn.discordapp.com/media/v1/collectibles-shop/ITEM_ID/static", "stock": null, "sellable": true, "available_until": null } ``` Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Quote a cart Validate a cart and calculate its total without spending balance. Base URL: https://api.decorate.gg/v1 `POST /v1/quote` Required scope: catalogue:read ## Parameters - `items` (array, required): Between 1 and 25 distinct items. Maximum order total: 500,000 cents ($5,000). - `items[].id` (string, required): An item ID from the catalogue. - `items[].qty` (integer): Quantity from 1 to 10; defaults to 1. - `items[].expected_price_cents` (integer): The unit price shown to your customer, in USD cents. A mismatch returns a price_changed line status on quotes and rejects order creation. ## Request body ```json { "items": [ { "id": "ITEM_ID", "qty": 1, "expected_price_cents": 250 } ] } ``` ## Example response ```json { "ok": true, "total_cents": 250, "currency": "USD", "lines": [ { "id": "ITEM_ID", "name": "Example decoration", "qty": 1, "unit_price_cents": 250, "line_total_cents": 250, "status": "ok" } ] } ``` Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Create an order Purchase the whole cart from your balance. Base URL: https://api.decorate.gg/v1 `POST /v1/orders` Required scope: orders:write ## Parameters - `idempotency_key` (string, required): Your unique reference for this purchase: 8–100 letters, numbers, periods, underscores, colons, or hyphens. Reuse it only when retrying the same order. - `items` (array, required): The same item shape used by Quote a cart, with 1–25 distinct items and quantities of 1–10. The entire cart must be available. ## Request body ```json { "idempotency_key": "your-order-1042", "items": [ { "id": "ITEM_ID", "qty": 1, "expected_price_cents": 250 } ] } ``` ## Example response ```json { "id": "ORDER_ID", "status": "paid", "total_cents": 250, "currency": "USD", "created_at": "2026-10-15T12:00:00Z", "delivered_at": null, "lines": [ { "id": "ITEM_ID", "name": "Example decoration", "qty": 1, "unit_price_cents": 250 } ], "links": [] } ``` Note: This endpoint spends your real balance. Quote the cart first. Note: Retries return gift links only when the key also has orders:read. A write-only key receives links:[] even for a delivered order. Note: Deliver gift links through your own integration. The API does not accept a recipient ID or send a customer DM for you. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # List your orders Find your API purchases, newest first. Base URL: https://api.decorate.gg/v1 `GET /v1/orders` Required scope: orders:read ## Parameters - `limit` (integer): 1–100 orders; defaults to 25. - `offset` (integer): Number of orders to skip. ## Example response ```json { "orders": [], "total": 0, "limit": 25, "offset": 0 } ``` Note: Lists return order summaries without gift links. Retrieve an individual order for its purchased items and gift links. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Retrieve an order Check progress and collect delivered gift links. Base URL: https://api.decorate.gg/v1 `GET /v1/orders/{id}` Required scope: orders:read ## Parameters - `id` (string · path, required): Your decorate order ID. ## Example response ```json { "id": "ORDER_ID", "status": "delivered", "total_cents": 250, "currency": "USD", "created_at": "2026-10-15T12:00:00Z", "delivered_at": "2026-10-15T12:05:00Z", "lines": [ { "id": "ITEM_ID", "name": "Example decoration", "qty": 1, "unit_price_cents": 250 } ], "links": [ "https://discord.gift/EXAMPLE" ] } ``` Note: Order states are paid (awaiting delivery), delivered (gift links ready), and cancelled (refunded before delivery). Note: Only orders belonging to your account are accessible. Unknown and unauthorized order IDs both return 404. Note: Treat gift links as sensitive. Anyone with a valid unclaimed link may redeem it. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Receive events Receive signed order updates on your server. Base URL: https://api.decorate.gg/v1 Register your HTTPS endpoint in the API dashboard. Each event has a unique ID. Store that ID and process each event once; delivery may be retried. Verify the signature against the raw request body before parsing JSON. The signing secret is separate from your API key and is shown only when created or rotated. The signature covers timestamp, event ID, and raw body, separated by periods. Reject stale timestamps, compare signatures in constant time, and reply with a successful HTTP status after accepting the event. Note: Use Retrieve an order as your source of truth if an event is delayed or missed. Note: Public HTTPS endpoints are required. Private networks, localhost, and redirects are not supported. Note: Events: order.accepted, order.delivered, order.cancelled, catalogue.changed, and test. Events have id, event, created_at, and data fields. Order events contain data.order. Note: Failed delivery retries start at 1 minute, double up to an hour, and stop after 12 attempts or 24 hours. Events may arrive out of order; retrieve the order for its current state. ## Verify a webhook in Node.js ```js import { createHmac, timingSafeEqual } from 'node:crypto'; // rawBody must be the exact received bytes, before JSON parsing. function verifyWebhook(headers, rawBody, secret) { const timestamp = headers.get('X-Webhook-Timestamp') || ''; const eventId = headers.get('X-Webhook-Id') || ''; const signature = headers.get('X-Webhook-Signature') || ''; if (!/^\d+$/.test(timestamp) || !eventId) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; if (!/^[a-f0-9]{64}$/.test(signature)) return false; const expected = createHmac('sha256', secret) .update(timestamp + '.' + eventId + '.') .update(rawBody).digest(); return timingSafeEqual(expected, Buffer.from(signature, 'hex')); } // After verification, deduplicate the event ID in durable storage. ``` ## Verify a webhook in Python ```python import hashlib, hmac, re, time def verify_webhook(headers, raw_body: bytes, secret: str) -> bool: ts = headers.get('X-Webhook-Timestamp', '') event_id = headers.get('X-Webhook-Id', '') signature = headers.get('X-Webhook-Signature', '') if not ts.isdigit() or not event_id: return False if not re.fullmatch('[a-f0-9]{64}', signature): return False if abs(time.time() - int(ts)) > 300: return False canonical = (ts + '.' + event_id + '.').encode() + raw_body expected = hmac.new(secret.encode(), canonical, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) # Verify before parsing JSON, then deduplicate the event ID. ``` --- # Webhook health Check your endpoint and recent delivery health. Base URL: https://api.decorate.gg/v1 `GET /v1/webhook` Required scope: webhooks:read ## Example response ```json { "webhook_url": "https://your-shop.example/webhooks/decorate", "pending": 0, "failed": 0, "last_delivered_at": "2026-10-15T12:05:00Z", "last_failure_at": null, "last_error": null } ``` Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Set an endpoint Send events to your public HTTPS endpoint. Base URL: https://api.decorate.gg/v1 `POST /v1/webhook` Required scope: webhooks:write ## Parameters - `url` (string | null, required): A public HTTPS endpoint that accepts JSON POST requests, or null to stop deliveries. ## Request body ```json { "url": "https://your-shop.example/webhooks/decorate" } ``` Note: Setting a URL requires both webhooks:write and orders:read because the destination receives your private order details and delivered gift links. Sending null only requires webhooks:write. Note: The response includes a signing secret only when a new endpoint is created. Save it immediately. Use the dashboard to rotate the separate webhook secret. Note: Changing the endpoint URL cancels deliveries queued for the previous destination. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Remove an endpoint Stop webhook deliveries for your account. Base URL: https://api.decorate.gg/v1 `DELETE /v1/webhook` Required scope: webhooks:write Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers. --- # Send a test event Check your endpoint and signature verification end to end. Base URL: https://api.decorate.gg/v1 `POST /v1/webhook/test` Required scope: webhooks:write ## Request body ```json {} ``` ## Example response ```json { "ok": true, "queued": true } ``` Note: This sends a test event to your saved endpoint. It does not create an order or spend balance. Authentication: HMAC-SHA256 over METHOD + newline + exact pathname/query + newline + timestamp + newline + nonce + newline + SHA256(raw body). Headers: X-Api-Key, X-Api-Timestamp, X-Api-Nonce, X-Api-Signature. Secrets stay on your server. See /developers?section=authentication for working helpers.