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 in X-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 the v1: 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.