Skip to main content
Portfolio Wallet withdrawals and strategy updates may require customer approval via Turnkey before transactions can be broadcast.

Approval lifecycle

  1. You create a withdrawal or strategy update.
  2. The system sources liquidity (unwind/bridge as needed).
  3. When ready, Braid creates a Turnkey signing activity for the payout leg.
  4. The payout leg transitions to pending_customer_approval.
  5. You approve the activity via Turnkey SDK.
  6. The leg transitions through pending_broadcastbroadcastedcompleted.

Head-of-line gating

Braid enforces one outstanding customer approval per walletId + chain. If you create multiple withdrawals quickly, later ones queue behind the earlier approval. This is represented in the withdrawal response as:
  • customerApprovalState: "queued" — blocked behind a prior approval
  • blockedBy — describes the blocking payout leg
  • The waiting payout leg shows status: "waiting_for_prior_approval"
Once the blocking payout becomes terminal, Braid wakes the next queued withdrawal.

Automation patterns

  1. Subscribe to portfolio_wallet.withdrawal.payout.status_changed.
  2. When you see status: "pending_customer_approval", the payload includes turnkeyActivityId — approve it via Turnkey.
  3. When you see status: "completed", the payout leg is finished.
app.post("/braid/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const event = JSON.parse(req.body);

  if (event.event === "portfolio_wallet.withdrawal.payout.status_changed") {
    const { payout } = event;

    if (payout.status === "pending_customer_approval" && payout.turnkeyActivityId) {
      await turnkey.apiClient().approveActivity({
        organizationId: process.env.TURNKEY_ORGANIZATION_ID,
        fingerprint: payout.turnkeyActivityId,
      });
    }
  }

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

Polling-driven

  1. Poll GET /v2/portfolio-wallets/:id/withdrawals/:withdrawalId.
  2. Check customerApprovalState:
    • "required": customer action needed now
    • "queued": blocked behind a prior approval (check blockedBy)
    • "not_required": still sourcing liquidity or no approval needed
Use webhooks when available, with polling as a backstop (webhooks are best-effort delivery).

Turnkey SDK setup

const { Turnkey } = require("@turnkey/sdk-server");

const turnkey = new Turnkey({
  apiBaseUrl: "https://api.turnkey.com",
  apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,
  apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
  defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,
});

// Approve a pending activity
const result = await turnkey.apiClient().approveActivity({
  organizationId: process.env.TURNKEY_ORGANIZATION_ID,
  fingerprint: activityId,  // from payout webhook's turnkeyActivityId field
});
The organizationId is your Turnkey organization ID (provided during onboarding). The fingerprint is the turnkeyActivityId from the payout webhook payload or the payout leg response. See Webhooks for webhook registration and verification.

Next steps