Skip to main content
Subscribe to webhooks to receive real-time notifications about balance changes, deposits, withdrawals, payouts, positions, and strategy updates.

Register a webhook

curl -X POST "$BASE_URL/webhooks/registrations" \
  -H "Authorization: Bearer $BRAID_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/braid/webhook",
    "eventTypes": [
      "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"
    ]
  }'
Response (201 Created):
{
  "id": "wh_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "url": "https://example.com/braid/webhook",
  "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"
  ],
  "createdAt": "2026-02-05T08:00:00.000Z",
  "secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
}
The secret is a 64-character hex string. It is only returned on creation. Store it immediately in an environment variable or secret manager — you cannot retrieve it later.

List webhook registrations

curl "$BASE_URL/webhooks/registrations" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"
{
  "endpoints": [
    {
      "id": "wh_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "url": "https://example.com/braid/webhook",
      "events": ["portfolio_wallet.balance.updated", "portfolio_wallet.deposit.status_changed"],
      "createdAt": "2026-02-05T08:00:00.000Z"
    }
  ]
}
The secret is not included in list responses.

Delete a webhook registration

curl -X DELETE "$BASE_URL/webhooks/registrations/$WEBHOOK_ID" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"
{
  "id": "wh_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "deleted": true
}
Deletion is a soft delete — the registration is disabled and stops receiving events. Existing delivery history is preserved for auditing.

Portfolio Wallet events

EventFires when
portfolio_wallet.balance.updatedWallet balance changes (deposits settling, yield accruing, rebalances)
portfolio_wallet.deposit.status_changedA deposit transitions status (confirmed → processing → completed)
portfolio_wallet.withdrawal.status_changedA withdrawal transitions status
portfolio_wallet.withdrawal.payout.status_changedA payout leg transitions status (useful for approval automation)
portfolio_wallet.position.updatedA position’s value, weight, or rate changes
portfolio_wallet.strategy.status_changedA strategy update progresses or completes

Event payloads

portfolio_wallet.balance.updated

{
  "event": "portfolio_wallet.balance.updated",
  "observedAt": "2026-02-05T10:05:00.000Z",
  "wallet": {
    "id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "label": "Treasury Portfolio",
    "balance": {
      "totalUsd": "82500.00",
      "withdrawableUsd": "82500.00",
      "earnedUsd": "1250.00"
    },
    "strategy": {
      "allocations": [
        { "positionKey": "usdz", "pct": 60 },
        { "positionKey": "rlp", "pct": 40 }
      ],
      "status": "aligned"
    },
    "positions": [
      { "positionKey": "usdz", "name": "Anzen USDz", "valueUsd": "52500.00" },
      { "positionKey": "rlp", "name": "Resolv LP", "valueUsd": "30000.00" }
    ]
  }
}

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",
    "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "status": "processing",
    "chain": "ethereum",
    "token": "usdc",
    "amount": "50000.00",
    "legStatuses": {
      "usdz": "completed",
      "rlp": "processing"
    }
  }
}

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",
    "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "amountUsd": "65000.00",
    "feeUsd": "0.00",
    "destinationChain": "ethereum",
    "destinationToken": "usdc",
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "status": "processing",
    "txHash": "0xabc123...",
    "payouts": [
      {
        "id": "0c7c6fcb-3c49-4f6d-9a8d-1d2b8d1ef7b0",
        "legKey": "pos_usdz_001",
        "status": "processing",
        "amount": "65000.00",
        "token": "usdc",
        "chain": "ethereum",
        "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "txHash": "0xabc123..."
      }
    ]
  }
}

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",
    "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "legKey": "pos_usdz_001",
    "status": "pending_customer_approval",
    "amount": "35000.00",
    "token": "usdc",
    "chain": "ethereum",
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "turnkeyActivityId": "act_abc123"
  }
}
When status is pending_customer_approval, the payload includes turnkeyActivityId — the Turnkey activity ID to approve. See Transaction Approvals for the approval flow.

portfolio_wallet.position.updated

{
  "event": "portfolio_wallet.position.updated",
  "observedAt": "2026-02-05T10:20:00.000Z",
  "position": {
    "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "positionKey": "usdz",
    "name": "Anzen USDz",
    "valueUsd": "52500.00",
    "targetWeightBps": 6000,
    "currentRateBps": 330
  }
}

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",
    "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
    "requestId": "6d74dc1c-f1b5-4f2c-8f33-9f7b9e27cd4b",
    "status": "completed",
    "allocations": [
      { "positionKey": "usdz", "pct": 50 },
      { "positionKey": "rlp", "pct": 50 }
    ]
  }
}

Delivery and retries

  • Success (2xx): delivery complete.
  • Server error (5xx) or network failure: retried with exponential backoff for up to 24 hours.
  • Client error (4xx): permanent failure, not retried. Fix your endpoint and subsequent events will deliver normally.
  • Timeout: your endpoint must respond within 30 seconds or the delivery is treated as a failure and retried.
Events may arrive out of order. Use observedAt timestamps and resource status fields to reconcile state rather than relying on delivery order.

Delivery headers

Braid POSTs to your callbackUrl with these headers:
HeaderDescription
Content-Typeapplication/json
Braid-Event-IdUnique event identifier (use for deduplication)
Braid-Event-TypeEvent type (e.g. portfolio_wallet.balance.updated)
Braid-SignatureHMAC signature for verification

Signature verification

Braid uses a Stripe-style HMAC scheme:
  • Header format: Braid-Signature: t=<unix_timestamp>,v1=<hex_hmac>
  • HMAC: v1 = HMAC_SHA256(key = signingSecret, message = t + "." + rawBody)
  • rawBody is the exact request body string (before JSON.parse).

Node / Express 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 format");

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

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

  const computed = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`)
    .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 signature");
  }
}

const app = express();

app.post("/braid/webhook", express.raw({ type: "application/json" }), (req, res) => {
  try {
    verifyBraidSignature(
      req.body.toString("utf8"),
      req.get("Braid-Signature"),
      process.env.BRAID_WEBHOOK_SIGNING_SECRET,
    );
  } catch (err) {
    return res.status(400).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body);
  // Handle event...
  res.status(200).json({ received: true });
});

Integration checklist

  1. Register a webhook and store secret securely.
  2. Verify every request with HMAC and enforce a timestamp window.
  3. Respond with 200 quickly — process events asynchronously.
  4. Deduplicate using Braid-Event-Id — your endpoint may receive the same event more than once.
  5. Log Braid-Event-Id and Braid-Event-Type for debugging.
  6. Use HTTPS — Braid only delivers to HTTPS endpoints.

Next steps