Skip to main content

Treasury Controls

Money-out controls for treasury and business accounts. Secret key required.

Background: a claim always pays the account's own wallet — that is guaranteed on-chain and needs no controls. Moving funds to any other address requires the flow on this page:

  1. The destination must be on the account's allowlist — new entries activate after a 24-hour timelock.
  2. If your organization has dual approval enabled, every withdrawal must be approved by a different API key than the one that created it.
  3. The approved withdrawal yields an unsigned transfer transaction — your multisig signs it like any other transaction.

Withdrawal lifecycle: pending_approval → built → submitted → confirmed, with expired when the 24-hour approval window lapses. A successful approval builds the transfer immediately, so the status moves straight to built; on-chain progress is tracked through the withdrawal's intentId. Without dual approval, withdrawals start at built.


POST /v1/treasury/allowlist

Add a destination address. Supports Idempotency-Key.

Body: {accountId, chain, address, label?}label ≤128 chars.

{ "id": "al_01h…", "status": "pending_activation", "activatesAt": "2026-06-16T08:00:00Z" }

Activation is lazy: an entry flips from pending_activation to active the first time it is read or used after the timelock elapses, and the treasury.allowlist.activated webhook fires then.

Errors: conflict (address already listed for this account) · invalid_request (account is not treasury/business).

GET /v1/treasury/allowlist?accountId=…

List entries as {data: […]}.

DELETE /v1/treasury/allowlist/:id

Removal is immediate (removing an address is risk-reducing; only additions are timelocked).


POST /v1/treasury/withdrawals

Create a withdrawal. Supports Idempotency-Key.

Body: {accountId, allowlistId, amount}amount is a positive decimal string; the allowlist entry must be active.

  • With dual approval: response {id: "twd_…", status: "pending_approval", amount} and a treasury.withdrawal.pending_approval webhook fires.
  • Without: response includes status: "built", an intentId, and the unsigned transaction immediately.

Errors: invalid_request (entry still inside the 24 h timelock) · not_found (entry unknown, removed, or belongs to a different account).

POST /v1/treasury/withdrawals/:id/approve

Must be called with a different secret key than the creator's — same key returns permission_denied.

{
"id": "twd_01h…", "status": "built", "intentId": "txi_…",
"transaction": { "format": "sui:tx-bytes-base64", "data": "…" }
}

Sign and submit like any transaction; track on-chain progress via the intentId. A treasury.withdrawal.approved webhook fires. Approvals expire after 24 h (status: "expired", invalid_request on a late approve).

GET /v1/treasury/withdrawals

Query: accountId=, status=, limit= (default 20, max 100). Items include approvals[] {keyId, keyPrefix, at} for audit.


Webhooks

treasury.allowlist.activated · treasury.withdrawal.pending_approval · treasury.withdrawal.approved · treasury.withdrawal.confirmed.