Authentication
SKUU Connect uses two mechanisms, and which one applies depends on the direction of the call and on whether the request has a body.
Which mechanism applies where
| Direction | Request | What proves it |
|---|---|---|
| SKUU to the store | GET /products | Authorization: Bearer <token> |
| SKUU to the store | Any request with a body | Bearer token plus X-SKUU-Signature |
| The store to SKUU | Any event | X-SKUU-Signature |
The store issues the bearer token SKUU sends. The HMAC secret is one random value per environment, shared by both sides.
The signature
X-SKUU-Signature: sha256=<lowercase hex of HMAC-SHA256(secret, raw body bytes)>To verify, in this order:
- Read the raw request bytes. Do not parse, re-serialize or trim them first.
- Compute HMAC-SHA256 over those bytes with the shared secret as UTF-8.
- Hex-encode the 32-byte digest in lowercase and prefix
sha256=. - Compare with the header using a constant-time comparison.
- Only then parse the JSON.
Whitespace, key order and escaping change the digest. Sign the exact bytes sent. Lowercase hex is the contract; SKUU tolerates uppercase.
Test vector
secret: skuu_test_secret
body: {"variant_id":"V-123-38-BLK","location_id":"900","available_quantity":4}
header: sha256=4d87a87bf1603abb826636239e62cedc6b15990fd609f3f5afb15dde81e5acc4The body is exactly 72 bytes with no trailing newline, and if the signing code produces this header the store's signing is right.
import hashlib, hmac
def sign(secret: str, raw_body: bytes) -> str:
return "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
def verify(secret: str, raw_body: bytes, header: str) -> bool:
return hmac.compare_digest(sign(secret, raw_body), (header or "").lower())const crypto = require("crypto");
function sign(secret, rawBody) {
return "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
}
function verify(secret, rawBody, header) {
const a = Buffer.from(sign(secret, rawBody));
const b = Buffer.from((header || "").toLowerCase());
return a.length === b.length && crypto.timingSafeEqual(a, b);
}BODY='{"spec_version":"1","event":"inventory.updated","event_id":"evt_01JABC123","occurred_at":"2026-07-17T12:00:00Z","data":{"variant_id":"V-123-38-BLK","location_id":"1","available_quantity":2}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SKUU_SECRET" | sed 's/^.* //')
curl -X POST "https://sync.skuu.net/webhooks/connect/$SHOP_SLUG" \
-H "Content-Type: application/json" \
-H "X-SKUU-Signature: sha256=$SIG" \
--data-binary "$BODY"A bad signature
An unsigned request, a wrong signature and an unknown shop_slug all get the same 401 body, {"detail": "unauthorized"}. SKUU does not say which.
Rotating the secret
The request format has no key id, so a rotation is a small ceremony per environment: agree a moment, stage the new value on both sides, switch, verify, then revoke the old value. Never rotate one side alone.
The next page is Errors and retries.
Updated 22 days ago
