BluebulbAPI
    Get started

    Verifying responses

    Orbita signs its responses to your signed requests with its own RSA key. Verifying the signature proves a response really came from Orbita and wasn't changed in transit.

    1. Get Orbita's public key

    Fetch it from Get Orbita's public key — no authentication needed. Cache it, and refresh it periodically (or when verification starts failing) so you pick up a key rotation.

    curl "{baseUrl}/api/public-key"

    2. Read the signature headers

    X-Response-Signature
    Base64 RSA-SHA256 (PKCS#1 v1.5) signature.
    X-Response-Signature-Timestamp
    Unix time in milliseconds when Orbita signed the response.

    3. Rebuild the signed string

    Join these five values with a newline (\n) — the same shape as a request signature, plus the status code:

    METHOD
    The method of your request, upper case.
    pathWithQuery
    The path and query string of your request, exactly as you sent it.
    statusCode
    The HTTP status of the response, e.g. 200. This stops a success response being swapped for an error, or vice versa.
    timestampMs
    The value of X-Response-Signature-Timestamp.
    sha256hex(body)
    SHA-256 of the exact response body bytes you received, hex-encoded.

    4. Verify

    Verify the signature with Orbita's public key, and reject any response whose timestamp is more than 5 minutes from your clock. The timestamp only limits how old a replayed response can be — it doesn't prove the response is current. The signature isn't tied to your specific request, so a captured response (for example, a payout-status GET) can be replayed against an identical request within those 5 minutes. Treat a verified status as true as of its timestamp, not necessarily now.

    const crypto = require("crypto");
    
    const MAX_AGE_MS = 5 * 60 * 1000;
    
    // res: the fetch Response; bodyText: await res.text() -- the exact bytes received.
    function verifyResponse(method, pathWithQuery, res, bodyText, orbitaPublicKeyPem) {
      const signature = res.headers.get("x-response-signature");
      const timestampMs = res.headers.get("x-response-signature-timestamp");
      if (!signature || !timestampMs) return false;
    
      const ts = Number(timestampMs);
      if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > MAX_AGE_MS) return false;
    
      const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex");
      const payload = [method, pathWithQuery, String(res.status), timestampMs, bodyHash].join("\n");
      return crypto
        .createVerify("RSA-SHA256")
        .update(payload)
        .verify(orbitaPublicKeyPem, signature, "base64");
    }
    Verify the body as received. Read the raw response text first, verify it, then parse it as JSON. Re-serializing a parsed object won't reproduce the same bytes.
    • Only responses to requests that passed signature verification are signed. Get Orbita's public key itself is never signed.
    • If a response you expect to be signed arrives without signature headers, fails verification, or never arrives, don't act on it. Your request may still have been processed, so follow Retrying safely: for conversions, check List conversions before sending the request again, since a retry can create a duplicate; for payouts, retry with the original customerReference.