Skip to main content
You already know webhooks. This is how Braid’s variant works.

Register a webhook

curl -X POST "https://sandbox.trybraid.xyz/webhooks/registrations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/braid/webhook",
    "eventTypes": [
      "yield_wallet.deposit.status_changed",
      "yield_wallet.withdrawal.status_changed"
    ]
  }'
  • Response contains id, signingSecret, etc.
  • Store signingSecret on the server only (env var / secret manager).

Supported events

Yield wallet events:
  • yield_wallet.balance.updated
  • yield_wallet.deposit.status_changed
  • yield_wallet.withdrawal.status_changed
Portfolio wallet events:
  • portfolio_wallet.balance.updated
  • portfolio_wallet.deposit.status_changed
  • portfolio_wallet.withdrawal.status_changed
  • portfolio_wallet.withdrawal.payout.status_changed
  • portfolio_wallet.position.updated
  • portfolio_wallet.strategy.status_changed
Portfolio wallet status notes:
  • withdrawal.status commonly includes pending_liquidity, processing, queued_for_customer_approval, pending_customer_approval, pending_broadcast, broadcasted, completed, failed.
  • payout.status commonly includes planning, waiting_for_prior_approval, pending_customer_approval, pending_broadcast, broadcasted, processing, completed, failed, skipped.
  • Payout legs are exposed as a payouts array in API and webhook payloads.
  • A withdrawal is complete when the withdrawal status is completed and all payout legs are completed or skipped.
  • Only one customer approval can be outstanding per wallet+chain. When a withdrawal is blocked behind a prior approval, you may see withdrawal.status=queued_for_customer_approval, customerApprovalState="queued", and blockedBy populated.
Example payload for portfolio_wallet.balance.updated:
{
  "event": "portfolio_wallet.balance.updated",
  "observedAt": "2026-02-05T10:05:00.000Z",
  "wallet": {
    "id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "requestId": "0b7e3b4d-88a5-4a75-8c0f-3b7b4f9d9f1d",
    "label": "Treasury Portfolio",
    "blendedRateBps": 565,
    "status": "active",
    "createdAt": "2026-02-05T08:15:00.000Z",
    "updatedAt": "2026-02-05T10:05:00.000Z",
    "depositAddresses": {
      "arbitrum": "0x21246509968c4d24611f414560971AEc2e3A079B",
      "ethereum": "0x21246509968c4d24611f414560971AEc2e3A079B"
    },
    "globalWalletBalance": 82500,
    "targetPositions": [
      { "positionKey": "usdz", "targetWeightBps": 6000, "maxProcessingTime": "PT0H" },
      { "positionKey": "rlp", "targetWeightBps": 4000, "maxProcessingTime": "PT24H" }
    ],
    "actualPositions": [
      {
        "positionKey": "usdz",
        "asset": "usdz",
        "chain": "arbitrum",
        "balanceUnits": 52500,
        "valueUsd": 52500,
        "actualWeightBps": 6120,
        "targetWeightBps": 6000,
        "driftBps": 120,
        "updatedAt": "2026-02-05T10:06:00.000Z"
      },
      {
        "positionKey": "rlp",
        "asset": "rlp",
        "chain": "ethereum",
        "balanceUnits": 25000,
        "valueUsd": 30000,
        "actualWeightBps": 3880,
        "targetWeightBps": 4000,
        "driftBps": -120,
        "updatedAt": "2026-02-05T10:06:00.000Z"
      }
    ],
    "liquidityProfile": [
      {
        "maxProcessingTime": "PT0H",
        "weightBps": 6000
      },
      {
        "maxProcessingTime": "PT24H",
        "weightBps": 4000
      }
    ]
  }
}
Example payload for portfolio_wallet.deposit.status_changed:
{
  "event": "portfolio_wallet.deposit.status_changed",
  "observedAt": "2026-02-05T09:45:00.000Z",
  "deposit": {
    "id": "2a3ad0af-11b3-41d5-96c5-2b9d8799f1e2",
    "portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "status": "processing",
    "failureReason": null,
    "chain": "ethereum",
    "token": "usdc",
    "amount": 50000,
    "txHash": null,
    "createdAt": "2026-02-05T09:40:00.000Z",
    "updatedAt": "2026-02-05T09:45:00.000Z",
    "completedAt": null,
    "legStatuses": {
      "usdz": "completed",
      "rlp": "processing"
    }
  }
}
Example payload for portfolio_wallet.withdrawal.status_changed:
{
  "event": "portfolio_wallet.withdrawal.status_changed",
  "observedAt": "2026-02-05T11:05:00.000Z",
  "withdrawal": {
    "id": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
    "requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
    "portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "status": "broadcasted",
    "failureReason": null,
    "withdrawalAmount": 65000,
    "destinationToken": "usdc",
    "destinationChain": "ethereum",
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "payouts": [
      {
        "id": "0c7c6fcb-3c49-4f6d-9a8d-1d2b8d1ef7b0",
        "legKey": "payout",
        "status": "processing",
        "amount": 65000,
        "token": "usdc",
        "chain": "ethereum",
        "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "txHash": "0xabc123..."
      }
    ],
    "createdAt": "2026-02-05T11:00:00.000Z",
    "updatedAt": "2026-02-05T11:05:00.000Z",
    "completedAt": null
  }
}
Example payload for portfolio_wallet.withdrawal.payout.status_changed:
{
  "event": "portfolio_wallet.withdrawal.payout.status_changed",
  "observedAt": "2026-02-05T12:40:00.000Z",
  "payout": {
    "id": "0c7c6fcb-3c49-4f6d-9a8d-1d2b8d1ef7b0",
    "withdrawalId": "11b17950-1f5c-4d36-8f0d-0f3d1d0c6a45",
    "portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "legKey": "instant",
    "status": "completed",
    "amount": 35000,
    "token": "usdc",
    "chain": "ethereum",
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "createdAt": "2026-02-05T12:39:00.000Z",
    "updatedAt": "2026-02-05T12:40:00.000Z"
  }
}
Example payload for portfolio_wallet.position.updated:
{
  "event": "portfolio_wallet.position.updated",
  "observedAt": "2026-02-05T10:20:00.000Z",
  "position": {
    "portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "positionKey": "usdz",
    "asset": "usdz",
    "maxProcessingTime": "PT0H",
    "targetWeightBps": 6000,
    "balanceUnits": 52500,
    "valueUsd": 52500,
    "rateBps": 350
  }
}
Example payload for portfolio_wallet.strategy.status_changed:
{
  "event": "portfolio_wallet.strategy.status_changed",
  "observedAt": "2026-02-05T09:30:00.000Z",
  "strategy": {
    "id": "7b21a5e9-1f4a-4a79-b0fa-6a2a36b032b2",
    "portfolioWalletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "requestId": "6d74dc1c-f1b5-4f2c-8f33-9f7b9e27cd4b",
    "status": "completed",
    "turnkeyActivityId": "activity-8f8c1b7f3b24",
    "strategyConfig": {
      "positions": [
        {
          "positionKey": "usdz",
          "targetWeightBps": 5000,
          "maxProcessingTime": "PT0H",
          "asset": "usdz"
        },
        {
          "positionKey": "rlp",
          "targetWeightBps": 5000,
          "maxProcessingTime": "PT24H",
          "asset": "rlp"
        }
      ]
    },
    "createdAt": "2026-02-05T09:29:00.000Z"
  }
}

Delivery and headers

POST to your callbackUrl with:
  • Content-Type: application/json
  • Braid-Event-Id
  • Braid-Event-Type
  • Braid-Signature
Body is the JSON event payload (e.g. deposit or withdrawal status changes).

Signature scheme

Stripe-style HMAC:
  • Header: Braid-Signature: t=<unix_timestamp>,v1=<hex_hmac>
  • HMAC: v1 = HMAC_SHA256(key = signingSecret, message = t + "." + rawBody)
  • rawBody = exact request body string (before JSON.parse).
Example:
Braid-Signature: t=1765102592,v1=479360c4f785070f193ef1b2fb05f2e464b020a87c420972fce6b92239c4bf09

Node / Express verification example

const crypto = require("crypto");
const express = require("express");

function verifyBraidSignature(
  rawBody,
  signatureHeader,
  signingSecret,
  toleranceSeconds = 300,
) {
  if (!signatureHeader) throw new Error("Missing Braid-Signature header");

  const parts = signatureHeader.split(",");
  const tPart = parts.find((p) => p.startsWith("t="));
  const v1Part = parts.find((p) => p.startsWith("v1="));
  if (!tPart || !v1Part)
    throw new Error("Invalid Braid-Signature header format");

  const timestamp = Number(tPart.split("=")[1]);
  const signature = v1Part.split("=")[1];

  // Optional replay protection
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    throw new Error("Braid-Signature timestamp outside allowed window");
  }

  const payloadToSign = `${timestamp}.${rawBody}`;
  const computed = crypto
    .createHmac("sha256", signingSecret)
    .update(payloadToSign)
    .digest("hex");

  const expected = Buffer.from(computed, "utf8");
  const actual = Buffer.from(signature, "utf8");
  if (
    expected.length !== actual.length ||
    !crypto.timingSafeEqual(expected, actual)
  ) {
    throw new Error("Invalid Braid-Signature");
  }
}

const app = express();

// Use raw body so you can verify before parsing
app.post(
  "/braid/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const signatureHeader = req.get("Braid-Signature");
    const signingSecret = process.env.BRAID_WEBHOOK_SIGNING_SECRET;

    try {
      verifyBraidSignature(rawBody, signatureHeader, signingSecret);
    } catch (err) {
      console.error("Webhook signature verification failed:", err.message);
      return res.status(400).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(rawBody);

    if (event.event === "yield_wallet.deposit.status_changed") {
      // handle deposit status change
    }

    res.status(200).json({ received: true });
  },
);

Quick checklist

  • Register a webhook and persist signingSecret.
  • Verify every request with t + "." + rawBody and constant-time compare.
  • Enforce a timestamp window and use HTTPS.
  • Log Braid-Event-Id and Braid-Event-Type for debugging.