Developers

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:

FieldFormatNotes
api_key_identifierUUIDSent in every request; safe to log.
signing_keyBase64 (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

HeaderFormatDescription
AuthorizationMosaic-HMAC-SHA256 key-id=<uuid>,signature=<base64>Auth scheme + key identifier + canonical-string signature.
X-Mosaic-TimestampRFC 3339 UTC, e.g. 2026-05-29T14:22:33ZMust be within ±300 seconds of server time.
X-Mosaic-NonceUUID v7Single-use, retained for 10 minutes server-side.
Idempotency-KeyUp 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

StatusProblem type suffixMeaning
401authorization-missing / authorization-invalidAuthorization header missing or malformed.
401timestamp-skewX-Mosaic-Timestamp outside the ±300s window.
401signature-invalidHMAC mismatch.
401credential-unknown / credential-revokedkey-id not found or no longer active.
403scope-requiredCredential exists but lacks the scope this endpoint demands.
409nonce-replayX-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.