Integration API v1
Authentication
SQRv1 — HMAC-SHA256 request signing.
Overview
Every request must be signed using your API key and API secret. The signature covers the HTTP method, URL path, query string, timestamp, and a hash of the request body — so intercepted requests cannot be replayed and the body cannot be tampered with.
Required headers
| Header | Value |
|---|---|
Authorization |
SQRv1 Credential=<key>, Timestamp=<unix_seconds>, Signature=<hex> |
X-SQR-Content-Hash |
Lowercase hex SHA-256 of the raw request body. For requests with no body, use the SHA-256 of the empty string. |
Content-Type |
application/json for requests that include a body. |
The timestamp must be within ±5 minutes of the server clock.
Requests outside that window are rejected to prevent replay attacks.
Signing algorithm
Build the canonical string by joining these five values with newlines (\n):
METHOD\n
PATH\n
SORTED_QUERY_STRING\n
UNIX_TIMESTAMP_SECONDS\n
X_SQR_CONTENT_HASH
| Field | Description |
|---|---|
METHOD | Uppercase HTTP method — GET, POST, etc. |
PATH | URL path only, no host, no query string — e.g. /api/integration/v1/devices |
SORTED_QUERY_STRING | Query parameters sorted by key and joined with & — e.g. foo=1&z=2. Empty string if no query params. |
UNIX_TIMESTAMP_SECONDS | Current Unix time in seconds as a decimal integer string. |
X_SQR_CONTENT_HASH | Lowercase hex SHA-256 of the raw request body bytes. For empty/no body: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
Then compute:
Signature = HMAC-SHA256(api_secret, canonical_string) → lowercase hex
Example
# Signing GET /api/integration/v1/devices with no body
canonical = "GET\n/api/integration/v1/devices\n\n1700000000\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
Authorization: SQRv1 Credential=my-api-key, Timestamp=1700000000, Signature=<hmac_hex>
X-SQR-Content-Hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Signing functions
Copy the function for your language into your codebase. Each implementation is self-contained and uses only the standard library.
const crypto = require("crypto");
/**
* Sign a ScanSeqr Integration API request.
* Returns the two headers that must be added to the request.
*/
function signRequest({ key, secret, method, path, query = "", body = "" }) {
const timestamp = Math.floor(Date.now() / 1000);
const contentHash = crypto
.createHash("sha256")
.update(body, "utf8")
.digest("hex");
const sortedQuery = query
? new URLSearchParams(
[...new URLSearchParams(query).entries()].sort(([a], [b]) => a.localeCompare(b))
).toString()
: "";
const canonical = [method.toUpperCase(), path, sortedQuery, timestamp, contentHash].join("\n");
const signature = crypto
.createHmac("sha256", secret)
.update(canonical, "utf8")
.digest("hex");
return {
Authorization: `SQRv1 Credential=${key}, Timestamp=${timestamp}, Signature=${signature}`,
"X-SQR-Content-Hash": contentHash,
};
}
// Usage — GET request (no body)
const headers = signRequest({
key: "YOUR_API_KEY",
secret: "YOUR_API_SECRET",
method: "GET",
path: "/api/integration/v1/devices",
});
// Usage — POST request with a JSON body
const body = JSON.stringify({ name: "Key for Bob", groupId: "...", startTime: "...", endTime: "..." });
const postHeaders = signRequest({
key: "YOUR_API_KEY",
secret: "YOUR_API_SECRET",
method: "POST",
path: "/api/integration/v1/accesskey",
body,
});
import { createHash, createHmac } from "crypto";
interface SignOptions {
key: string;
secret: string;
method: string;
path: string;
query?: string;
body?: string;
}
interface SignedHeaders {
Authorization: string;
"X-SQR-Content-Hash": string;
}
function signRequest({ key, secret, method, path, query = "", body = "" }: SignOptions): SignedHeaders {
const timestamp = Math.floor(Date.now() / 1000);
const contentHash = createHash("sha256").update(body, "utf8").digest("hex");
const sortedQuery = query
? new URLSearchParams(
[...new URLSearchParams(query).entries()].sort(([a], [b]) => a.localeCompare(b))
).toString()
: "";
const canonical = [method.toUpperCase(), path, sortedQuery, timestamp, contentHash].join("\n");
const signature = createHmac("sha256", secret).update(canonical, "utf8").digest("hex");
return {
Authorization: `SQRv1 Credential=${key}, Timestamp=${timestamp}, Signature=${signature}`,
"X-SQR-Content-Hash": contentHash,
};
}
export { signRequest };
import hashlib
import hmac
import time
from urllib.parse import urlencode, parse_qsl
def sign_request(key: str, secret: str, method: str, path: str,
query: str = "", body: str = "") -> dict:
"""
Sign a ScanSeqr Integration API request.
Returns a dict of headers to add to the request.
"""
timestamp = int(time.time())
content_hash = hashlib.sha256(body.encode()).hexdigest()
sorted_query = urlencode(sorted(parse_qsl(query))) if query else ""
canonical = "\n".join([
method.upper(),
path,
sorted_query,
str(timestamp),
content_hash,
])
signature = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
return {
"Authorization": f"SQRv1 Credential={key}, Timestamp={timestamp}, Signature={signature}",
"X-SQR-Content-Hash": content_hash,
}
# Usage — GET request
import requests
headers = sign_request(
key="YOUR_API_KEY",
secret="YOUR_API_SECRET",
method="GET",
path="/api/integration/v1/devices",
)
response = requests.get("https://api-uat.scanseqr.com/api/integration/v1/devices", headers=headers)
# Usage — POST with a JSON body
import json
body = json.dumps({"name": "Key for Bob", "groupId": "...", "startTime": "...", "endTime": "..."})
headers = sign_request(
key="YOUR_API_KEY",
secret="YOUR_API_SECRET",
method="POST",
path="/api/integration/v1/accesskey",
body=body,
)
headers["Content-Type"] = "application/json"
response = requests.post("https://api-uat.scanseqr.com/api/integration/v1/accesskey",
headers=headers, data=body)
<?php
/**
* Sign a ScanSeqr Integration API request.
* Returns an array of headers to add to the request.
*/
function sqrv1_sign(string $key, string $secret, string $method, string $path,
string $query = '', string $body = ''): array
{
$timestamp = time();
$contentHash = hash('sha256', $body);
$sortedQuery = '';
if ($query !== '') {
parse_str($query, $params);
ksort($params);
$sortedQuery = http_build_query($params);
}
$canonical = implode("\n", [
strtoupper($method),
$path,
$sortedQuery,
(string) $timestamp,
$contentHash,
]);
$signature = hash_hmac('sha256', $canonical, $secret);
return [
'Authorization' => "SQRv1 Credential={$key}, Timestamp={$timestamp}, Signature={$signature}",
'X-SQR-Content-Hash' => $contentHash,
];
}
// Usage — GET request
$headers = sqrv1_sign('YOUR_API_KEY', 'YOUR_API_SECRET', 'GET', '/api/integration/v1/devices');
$ch = curl_init('https://api-uat.scanseqr.com/api/integration/v1/devices');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(
fn($k, $v) => "$k: $v",
array_keys($headers), $headers
));
$response = curl_exec($ch);
// Usage — POST with a JSON body
$body = json_encode(['name' => 'Key for Bob', 'groupId' => '...', 'startTime' => '...', 'endTime' => '...']);
$headers = sqrv1_sign('YOUR_API_KEY', 'YOUR_API_SECRET', 'POST', '/api/integration/v1/accesskey', '', $body);
$headers['Content-Type'] = 'application/json';
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"sort"
"strings"
"time"
)
// SQRv1Sign returns the Authorization and X-SQR-Content-Hash header values
// for the given request parameters.
func SQRv1Sign(key, secret, method, path, query, body string) (authorization, contentHash string) {
timestamp := time.Now().Unix()
h := sha256.Sum256([]byte(body))
contentHash = hex.EncodeToString(h[:])
// Sort query parameters by key.
sortedQuery := ""
if query != "" {
params, _ := url.ParseQuery(query)
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+"="+params.Get(k))
}
sortedQuery = strings.Join(parts, "&")
}
canonical := strings.Join([]string{
strings.ToUpper(method),
path,
sortedQuery,
fmt.Sprintf("%d", timestamp),
contentHash,
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
sig := hex.EncodeToString(mac.Sum(nil))
authorization = fmt.Sprintf("SQRv1 Credential=%s, Timestamp=%d, Signature=%s", key, timestamp, sig)
return
}
// Usage — GET request
func exampleGet() {
auth, hash := SQRv1Sign("YOUR_API_KEY", "YOUR_API_SECRET", "GET", "/api/integration/v1/devices", "", "")
fmt.Println("Authorization:", auth)
fmt.Println("X-SQR-Content-Hash:", hash)
}
#!/usr/bin/env bash
# sqrv1_sign.sh — requires bash, openssl, date (GNU or macOS)
#
# Usage:
# source sqrv1_sign.sh
# sqrv1_sign GET /api/integration/v1/devices
# sqrv1_sign POST /api/integration/v1/accesskey '{"name":"Bob","groupId":"...","startTime":"...","endTime":"..."}'
SQR_API_KEY="YOUR_API_KEY"
SQR_API_SECRET="YOUR_API_SECRET"
SQR_BASE_URL="https://api-uat.scanseqr.com/api/integration/v1"
sqrv1_sign() {
local method="${1^^}" # uppercase
local path="$2"
local body="${3:-}"
local query="${4:-}"
local timestamp
timestamp=$(date -u +%s)
local content_hash
content_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $2}')
# Sort query params (simple key-sort; extend if values contain & or =)
local sorted_query=""
if [ -n "$query" ]; then
sorted_query=$(echo "$query" | tr '&' '\n' | sort | tr '\n' '&' | sed 's/&$//')
fi
local canonical="${method}
${path}
${sorted_query}
${timestamp}
${content_hash}"
local signature
signature=$(printf '%s' "$canonical" | openssl dgst -sha256 -hmac "$SQR_API_SECRET" -hex | awk '{print $2}')
local auth="SQRv1 Credential=${SQR_API_KEY}, Timestamp=${timestamp}, Signature=${signature}"
if [ -z "$body" ]; then
curl -s \
-H "Authorization: $auth" \
-H "X-SQR-Content-Hash: $content_hash" \
"${SQR_BASE_URL}${path}"
else
curl -s -X "$method" \
-H "Authorization: $auth" \
-H "X-SQR-Content-Hash: $content_hash" \
-H "Content-Type: application/json" \
-d "$body" \
"${SQR_BASE_URL}${path}"
fi
}
# Examples:
# sqrv1_sign GET /devices
# sqrv1_sign POST /accesskey '{"name":"Bob","groupId":"uuid","startTime":"2025-01-01T00:00:00Z","endTime":"2026-01-01T00:00:00Z"}'
Error responses
| Status | Meaning |
|---|---|
| 403 (no body) | Authentication failed. Check that the credential was created after SQRv1 was introduced, that the timestamp is within 5 minutes of server time, and that your signing implementation matches the algorithm above. |
| 401 | Authorization header missing or malformed. |
Use the Playground to generate a signed curl command
and verify your credentials without writing any code.