API Guide
Request Signing
HMAC-SHA-256 signing, required on every write-capable key.
Any key holding a write, delete, send, or manage scope is issued an HMAC secret alongside the key and must sign every request. Two read scopes also force signing because of the breadth of data they expose: communications:read and audit_log:read. All other read-only keys do not sign. The secret looks like:
mip_secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Building the signature
The signature is an HMAC-SHA-256 over a canonical string built from the request, keyed with your HMAC secret and hex-encoded. The canonical string is five parts joined with colons, starting with the literal v1:
timestamp = current unix seconds
canonical = "v1:" + timestamp + ":" + METHOD + ":" + path_with_query + ":" + sha256_hex(body)
signature = hex(hmac_sha256(secret, canonical))
timestamp— the same value you send inX-MIP-Timestamp.METHOD— uppercase (GET,POST,PATCH,DELETE).path_with_query— the path exactly as requested, starting at/v1/…, including the query string if there is one (e.g./v1/customers?limit=25).sha256_hex(body)— the SHA-256 of the exact body bytes you transmit. For requests with no body, hash the empty string:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Send the timestamp and signature as headers alongside the Bearer key:
Authorization: Bearer mip_live_...
X-MIP-Timestamp: 1786000000
X-MIP-Signature: 9a3f...
The timestamp must be within ±5 minutes of server time.
Node example
import crypto from "node:crypto";
function sign(secret, method, pathWithQuery, body = "") {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const canonical = ["v1", ts, method.toUpperCase(), pathWithQuery, bodyHash].join(":");
const signature = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
return { "X-MIP-Timestamp": ts, "X-MIP-Signature": signature };
}
Failure codes
All signing failures return 401:
signature_missing— the key requires signing and no signature was sent.signature_malformed— headers present but unparseable.signature_stale— the timestamp is outside the ±5-minute window. Sync your server clock with NTP.signature_invalid— the computed signature does not match. Almost always a canonical-string mismatch: check thev1:prefix and colon separators, that the path includes the query string exactly as sent, and that the body hash is over the exact bytes transmitted.signature_unverifiable— a server-side condition; safe to retry later, or contact support if persistent.replay_detected— that exact signature was already used. Generate a fresh timestamp per attempt; never replay a captured request.
Operational notes
Sign the bytes you actually send — serialize the body once, hash that string, and transmit the same string. Re-serializing between hashing and sending is the most common cause of signature_invalid. Secrets rotate with their key: rotating a key issues a new secret alongside the new key material, and the old pair stops working immediately.