Authentication
Every request is signed with HMAC-SHA256. There are no bearer tokens to expire, leak or refresh.
Why HMAC?
HMAC-SHA256 is our preferred authentication method for the Mosaic Hub API.
Credentials
Mosaic issues two values per integration:
| Field | Format | Notes |
|---|---|---|
api_key_identifier | UUID | Sent in every request; safe to log. |
signing_key | Base64 (32 random bytes) | Server-side secret; never log or transmit in plaintext. |
Keys are scoped (e.g. numbers.spending-methods.write) and tied to a single Mosaic Hub client. Lost or compromised keys can be revoked instantly.
Required headers
| Header | Format | Description |
|---|---|---|
Authorization | Mosaic-HMAC-SHA256 key-id=<uuid>,signature=<base64> | Auth scheme + key identifier + canonical-string signature. |
X-Mosaic-Timestamp | RFC 3339 UTC, e.g. 2026-05-29T14:22:33Z | Must be within ±300 seconds of server time. |
X-Mosaic-Nonce | UUID v7 | Single-use, retained for 10 minutes server-side. |
Idempotency-Key | Up to 64 chars (UUID v7 recommended) | Required on all write endpoints. |
Canonical string
Build a canonical string from six lines joined with \n (LF, not CRLF):
Canonical string
METHOD # uppercase, e.g. POST PATH # exact path, e.g. /v1/numbers-spending-methods SORTED_QUERY # query string with parameters sorted by name; empty string if none SHA256_HEX(BODY) # lowercase hex of SHA-256 of the raw request body (empty SHA for no body) TIMESTAMP # same value as X-Mosaic-Timestamp NONCE # same value as X-Mosaic-Nonce
The signature is base64(HMAC_SHA256(signing_key, canonical_string)). Use the raw decoded signing_key bytes; do not base64-decode twice.
Reference implementation - cURL
A minimal smoke test once you have generated the headers using one of the language snippets below (substitute the three placeholders with the values it emits):
Shell
curl -X POST https://hub.mosaic.co.za/api/v1/numbers-supplier-bills \
-H "Authorization: Mosaic-HMAC-SHA256 key-id=<ApiKeyIdentifier>,signature=<Signature>" \
-H "X-Mosaic-Timestamp: <Timestamp>" \
-H "X-Mosaic-Nonce: <Nonce>" \
-H "X-Mosaic-Request-Identifier: $(uuidgen)" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-binary @payload.json
Reference implementation - Node.js
JavaScript
import Crypto from "node:crypto";
function SignMosaicRequest(Method, Path, Query, BodyBuffer, ApiKeyIdentifier, SigningKeyBase64) {
const Timestamp = new Date().toISOString();
const Nonce = Crypto.randomUUID();
const SortedQuery = new URLSearchParams([...new URLSearchParams(Query || "")].sort()).toString();
const BodyHash = Crypto.createHash("sha256").update(BodyBuffer || Buffer.alloc(0)).digest("hex");
const Canonical = [Method.toUpperCase(), Path, SortedQuery, BodyHash, Timestamp, Nonce].join("\n");
const Signature = Crypto.createHmac("sha256", Buffer.from(SigningKeyBase64, "base64"))
.update(Canonical)
.digest("base64");
return {
Authorization: `Mosaic-HMAC-SHA256 key-id=${ApiKeyIdentifier},signature=${Signature}`,
"X-Mosaic-Timestamp": Timestamp,
"X-Mosaic-Nonce": Nonce
};
}
Reference implementation - PHP
PHP
<?php
function SignMosaicRequest($Method, $Path, $Query, $Body, $ApiKeyIdentifier, $SigningKeyBase64) {
$Timestamp = gmdate("Y-m-d\TH:i:s\Z");
$Nonce = bin2hex(random_bytes(16));
parse_str((string)$Query, $QueryArray);
ksort($QueryArray);
$SortedQuery = http_build_query($QueryArray);
$BodyHash = hash("sha256", (string)$Body);
$Canonical = implode("\n", [strtoupper($Method), $Path, $SortedQuery, $BodyHash, $Timestamp, $Nonce]);
$Signature = base64_encode(hash_hmac("sha256", $Canonical, base64_decode($SigningKeyBase64), true));
return [
"Authorization" => "Mosaic-HMAC-SHA256 key-id={$ApiKeyIdentifier},signature={$Signature}",
"X-Mosaic-Timestamp" => $Timestamp,
"X-Mosaic-Nonce" => $Nonce,
];
}
Reference implementation - Python
Python
import base64, hashlib, hmac, uuid
from datetime import datetime, timezone
from urllib.parse import parse_qsl, urlencode
def sign_mosaic_request(method, path, query, body, api_key_identifier, signing_key_base64):
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = uuid.uuid4().hex
sorted_query = urlencode(sorted(parse_qsl(query or "", keep_blank_values=True)))
body_bytes = body if isinstance(body, (bytes, bytearray)) else (body or "").encode("utf-8")
body_hash = hashlib.sha256(body_bytes).hexdigest()
canonical = "\n".join([method.upper(), path, sorted_query, body_hash, timestamp, nonce])
signature = base64.b64encode(
hmac.new(base64.b64decode(signing_key_base64), canonical.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
return {
"Authorization": f"Mosaic-HMAC-SHA256 key-id={api_key_identifier},signature={signature}",
"X-Mosaic-Timestamp": timestamp,
"X-Mosaic-Nonce": nonce,
}
Error responses
| Status | Problem type suffix | Meaning |
|---|---|---|
| 401 | authorization-missing / authorization-invalid | Authorization header missing or malformed. |
| 401 | timestamp-skew | X-Mosaic-Timestamp outside the ±300s window. |
| 401 | signature-invalid | HMAC mismatch. |
| 401 | credential-unknown / credential-revoked | key-id not found or no longer active. |
| 403 | scope-required | Credential exists but lacks the scope this endpoint demands. |
| 409 | nonce-replay | X-Mosaic-Nonce has already been used by this credential. |
Always verify the server's response in production against a real signing harness - the most common implementation bug is forgetting to base64-decode the secret before passing it to HMAC.