MARBIN← API referenceDevelopers
MARBIN · API guide

Webhooks

MARBIN tells your systems when something happens to your customers: a POST of a JSON event to each endpoint you register in the partner portal (API & webhooks), for the events you choose.

EventWhen
customer.joinedA customer finished registering (waiting for approval)
customer.approvedA customer was approved: trading starts
account.pausedA customer's trading was paused
account.resumedA customer's trading was resumed
trade.executedA buy or a sale was filled at the broker
stop.hitA holding was sold by its protective stop
alertSomething on a customer's account needs a look
customer.removedA customer was removed (or their invitation cancelled): MARBIN stops trading for them
engine.pausedMARBIN paused all trading (every customer): no new buys; stops stay at the broker
engine.resumedMARBIN resumed all trading
test.pingThe partner portal's Send a test event

What an event looks like

The same envelope for every event: id (unique; a retry carries the same one), type, created (Unix seconds), api_version, bank (your id at MARBIN), livemode (false from the sandbox) and data. A customer is named by customer_id (cus_…, random, the same in the customer API and your reports) and customer_ref, your own reference for them -- never a name, email, phone number or balance. engine.paused / engine.resumed name no customer: MARBIN paused or resumed trading for everyone (stops stay at the broker). Events never carry MARBIN's rules or thresholds: a sale's reason is one of manual, not_found_at_broker, rebalance, shariah, sold_by_customer, stop, take_profit, trailing_stop, withdrawal; an alert has a kind and a fixed message. practice is true for a practice (demo) account.

customer.approved

{
  "id": "evt_4f0c2b9a1d7e33c6a8b51e02",
  "type": "customer.approved",
  "created": 1792569601,
  "api_version": "1",
  "bank": "your-bank",
  "livemode": true,
  "data": {
    "customer": {
      "customer_id": "cus_7d1e04b9a2c35f68",
      "customer_ref": "YB-000123"
    },
    "status": "approved",
    "by": "bank"
  }
}

trade.executed

{
  "id": "evt_4f0c2b9a1d7e33c6a8b51e02",
  "type": "trade.executed",
  "created": 1792569601,
  "api_version": "1",
  "bank": "your-bank",
  "livemode": true,
  "data": {
    "customer": {
      "customer_id": "cus_7d1e04b9a2c35f68",
      "customer_ref": "YB-000123"
    },
    "practice": false,
    "trade": {
      "side": "sell",
      "symbol": "MSFT",
      "quantity": 3.0,
      "price": 412.5,
      "currency": "USD",
      "executed_at": 1792569600,
      "reason": "stop"
    }
  }
}

stop.hit

{
  "id": "evt_4f0c2b9a1d7e33c6a8b51e02",
  "type": "stop.hit",
  "created": 1792569601,
  "api_version": "1",
  "bank": "your-bank",
  "livemode": true,
  "data": {
    "customer": {
      "customer_id": "cus_7d1e04b9a2c35f68",
      "customer_ref": "YB-000123"
    },
    "practice": false,
    "trade": {
      "side": "sell",
      "symbol": "MSFT",
      "quantity": 3.0,
      "price": 412.5,
      "currency": "USD",
      "executed_at": 1792569600,
      "reason": "stop"
    }
  }
}

alert

{
  "id": "evt_4f0c2b9a1d7e33c6a8b51e02",
  "type": "alert",
  "created": 1792569601,
  "api_version": "1",
  "bank": "your-bank",
  "livemode": true,
  "data": {
    "customer": {
      "customer_id": "cus_7d1e04b9a2c35f68",
      "customer_ref": "YB-000123"
    },
    "practice": false,
    "alert": {
      "kind": "cash_short",
      "message": "The account's cash is below zero: no buys until it is back above zero.",
      "at": 1792569600
    }
  }
}

engine.paused

{
  "id": "evt_4f0c2b9a1d7e33c6a8b51e02",
  "type": "engine.paused",
  "created": 1792569601,
  "api_version": "1",
  "bank": "your-bank",
  "livemode": true,
  "data": {
    "status": "paused",
    "by": "marbin",
    "at": 1792569600
  }
}

Headers

HeaderValue
MARBIN-Signaturet=<unix seconds>,v1=<hex>
MARBIN-Event-Idthe event's id
MARBIN-Event-Typeits type
Content-Typeapplication/json
User-AgentMARBIN-Webhooks/1

Checking the signature

Each endpoint has its own signing secret (whsec_…), shown once when you add it. v1 is the HMAC-SHA256, in hex, of the timestamp, a full stop and the raw body: HMAC_SHA256(secret, t + "." + body). Recompute it over the body exactly as received, compare in constant time, and refuse a timestamp more than 5 minutes from your clock -- that stops a captured event being replayed. Several v1 values may come (a future secret change): accept a match with any.

Python

import hashlib
import hmac
import time


def verify_marbin(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    """True when a MARBIN webhook is genuine and fresh.

    secret: the endpoint's signing secret (whsec_...), from your secrets store.
    header: the MARBIN-Signature request header, "t=<unix seconds>,v1=<hex>".
    body:   the raw request body, exactly as received -- before any JSON parsing.
    """
    items = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
    ts = next((v for k, v in items if k == "t"), "")
    signatures = [v for k, v in items if k == "v1"]
    if not ts.isdigit() or not signatures:
        return False
    if abs(time.time() - int(ts)) > tolerance:      # too old (or from the future): a replay
        return False
    expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)
# Flask, for example: check first, answer 2xx quickly, then do the work (an event may come more than once).
@app.post("/marbin/events")
def marbin_events():
    body = request.get_data()                        # raw bytes
    if not verify_marbin(MARBIN_SECRET, request.headers.get("MARBIN-Signature", ""), body):
        return "", 400
    event = json.loads(body)
    if already_seen(event["id"]):                    # idempotency: retries carry the same id
        return "", 200
    queue_for_processing(event)
    return "", 200

Node.js

const crypto = require("crypto");

// True when a MARBIN webhook is genuine and fresh.
// secret: the endpoint's signing secret (whsec_...); header: the MARBIN-Signature header;
// rawBody: the request body as a Buffer, exactly as received (e.g. express.raw({ type: "application/json" })).
function verifyMarbin(secret, header, rawBody, toleranceSec = 300) {
  const items = String(header || "").split(",").map((p) => {
    const i = p.indexOf("=");
    return i < 0 ? ["", ""] : [p.slice(0, i).trim(), p.slice(i + 1).trim()];
  });
  const ts = (items.find(([k]) => k === "t") || [])[1] || "";
  const signatures = items.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!/^\d+$/.test(ts) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;   // a replay
  const expected = crypto.createHmac("sha256", secret).update(ts + ".").update(rawBody).digest("hex");
  return signatures.some((s) => s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

module.exports = { verifyMarbin };

Answering, retries and order