Request signing
Live keys require a signature by default. It proves the body was not altered in transit and bounds replay to five minutes.
The canonical string
{timestamp}.{METHOD}.{path-with-query}.{sha256hex(body)}
timestamp— Unix seconds (not milliseconds)METHOD— uppercase verbpath-with-query— path including the query string, exactly as sentsha256hex(body)— hex SHA-256 of the raw body; for no body, hash the empty string
Worked example — GET /v1/operations/lots?page=2, empty body, t=1735689600:
1735689600.GET./v1/operations/lots?page=2.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
HMAC-SHA256 that with the signing secret, hex-encode, and send:
x-mt-timestamp: 1735689600
x-mt-signature: t=1735689600,v1=<hex>
The t= inside the signature must equal the x-mt-timestamp header. They are
cross-checked so a fresh-looking header cannot wrap an old signed payload.
Examples
- Node SDK
- Python
- cURL
Pass signingSecret and it is handled per request, including on retries:
const client = new MineTech({
apiKey: process.env.MINETECH_API_KEY!,
signingSecret: process.env.MINETECH_SIGNING_SECRET!,
});
import hashlib, hmac, json, time, requests
def signed_request(method, path, secret, api_key, body=None):
raw = json.dumps(body, separators=(",", ":")) if body is not None else ""
timestamp = int(time.time())
body_hash = hashlib.sha256(raw.encode()).hexdigest()
canonical = f"{timestamp}.{method.upper()}.{path}.{body_hash}"
digest = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
return requests.request(
method,
f"https://api.minetech.rw{path}",
data=raw if body is not None else None,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"x-mt-timestamp": str(timestamp),
"x-mt-signature": f"t={timestamp},v1={digest}",
},
)
Note separators=(",", ":") — the bytes you hash must be the bytes you send.
#!/usr/bin/env bash
SECRET="$MINETECH_SIGNING_SECRET"
METHOD="GET"
PATH_WITH_QUERY="/v1/operations/lots?page=2"
BODY=""
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -r | awk '{print $1}')
CANONICAL="${TS}.${METHOD}.${PATH_WITH_QUERY}.${BODY_HASH}"
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -r | awk '{print $1}')
curl -X "$METHOD" "https://api.minetech.rw${PATH_WITH_QUERY}" \
-H "Authorization: Bearer $MINETECH_API_KEY" \
-H "x-mt-timestamp: ${TS}" \
-H "x-mt-signature: t=${TS},v1=${SIG}"
Troubleshooting
| Error | Cause |
|---|---|
signature_missing | Key requires signing; headers absent |
timestamp_missing | x-mt-timestamp absent or not an integer |
timestamp_mismatch | t= disagrees with the header |
timestamp_stale | Outside the 300s window — check your clock |
signature_mismatch | Digest differs — see below |
signature_mismatch is almost always one of:
- Milliseconds instead of seconds in the timestamp.
- A re-serialised body. Hash the exact bytes you transmit. If your HTTP client re-encodes JSON after you hash it, the digests differ.
- A path without its query string, or a URL-encoded difference between what you signed and what you sent.
- The API key used as the signing secret. They are two different values.
Verify your implementation against the worked example above — if it reproduces that
exact digest with secret secret, your construction is right.