> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Business Accounts Quickstart

> Go from a signed-in user to a quorum-approved signature on a business-account wallet, with a second signer and an admin approval along the way.

> [!NOTE]
>
> Business Accounts are in **early access**. See the [overview](https://docs.dynamic.xyz/docs/business-accounts/overview.md) for the model.

This quickstart runs one end-to-end path in a React app: create a business account, give it an EVM wallet, add a teammate as a second signer and an admin member (which exercises step-up authentication and the intent-signing retry), attach a quorum rule, then sign a message through the propose, approve, and resume flow. Each step links to the full guide for that operation.

## Prerequisites

* A Dynamic client initialized with your environment ID (see [Creating a Dynamic Client](https://docs.dynamic.xyz/docs/javascript/reference/client/create-dynamic-client.md) and [Initializing the Dynamic Client](https://docs.dynamic.xyz/docs/javascript/reference/client/initialize-dynamic-client.md))
* An authenticated user, because every business account call acts on behalf of the signed-in user
* Business Accounts access for your Dynamic account. Business Accounts are in early access, so [talk to Dynamic](https://www.dynamic.xyz/book-a-call) to get access before you build.
* Email sign-in turned on. In the [Dynamic developer console](https://console.dynamic.xyz/dashboard/log-in-user), turn on **Email** under **Sign-in Methods**.
* EVM turned on as a chain. In the [Dynamic developer console](https://console.dynamic.xyz/dashboard/chains-and-networks), turn on **EVM** under **Chains & Networks**. Without it, wallet creation and signing fail silently.
* `@dynamic-labs-sdk/react-hooks` installed and the app wrapped in `DynamicProvider`, as in the [React quickstart](https://docs.dynamic.xyz/docs/javascript/reference/react-quickstart.md)

**1. Create the business account**

`useCreateBusinessAccount` returns a mutation hook; `mutateAsync` resolves to the new account, whose `id` you need for every later call. The signed-in user becomes the owner.

```tsx theme={"system"}
import { useCreateBusinessAccount } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: createBusinessAccount } = useCreateBusinessAccount();

const account = await createBusinessAccount({ name: 'Acme Treasury' });
// account.id → the businessAccountId for all later steps
```

Expected outcome: `account.id` is set, and the caller is the account's owner.

**2. Create a wallet for the account**

`createWalletForBusinessAccount` mints an embedded wallet owned by the account and seats the caller as its first signer. Use the `useCreateWalletForBusinessAccount` hook.

```tsx theme={"system"}
import { useCreateWalletForBusinessAccount } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: createWalletForBusinessAccount } =
  useCreateWalletForBusinessAccount();

await createWalletForBusinessAccount({
  businessAccountId: account.id,
  chain: 'EVM',
});
```

Expected outcome: the account owns an EVM wallet, and the caller can already sign with it.

**3. Add a second signer**

Adding a signer exercises three SDK behaviors in one call:

1. **Step-up authentication.** Adding a signer requires an elevated token scoped to `business_account:signer:add` (see [Step-Up Authentication](https://docs.dynamic.xyz/docs/business-accounts/step-up-auth.md) for the model). Check first with `checkStepUpAuth`, then verify with a passkey or an email code, passing `requestedScopes`. The code below uses a passkey; if the user signed in with an email code, use the email code version after it.
2. **Intent signing.** When the SDK cannot construct the intent locally, for example when the target is identified by email, `addBusinessAccountSigner` throws `BusinessAccountIntentRequiredError`. Sign `error.intent` with `signBusinessAccountIntent` and retry with the returned `intent` and `intentSignature`.
3. **Governance.** When the account requires approvals for signer changes, the call returns a pending action instead of a share set. Narrow it with `isBusinessAccountActionRequired` and surface the proposal for approval.

```tsx theme={"system"}
import {
  authenticatePasskeyMFA,
  checkStepUpAuth,
  TokenScope,
} from '@dynamic-labs-sdk/client';
import {
  BusinessAccountIntentRequiredError,
  isBusinessAccountActionRequired,
  signBusinessAccountIntent,
} from '@dynamic-labs-sdk/client/waas';
import {
  useAddBusinessAccountSigner,
  useGetWalletAccounts,
} from '@dynamic-labs-sdk/react-hooks';

const { data: walletAccounts = [] } = useGetWalletAccounts();
const { mutateAsync: addBusinessAccountSigner } =
  useAddBusinessAccountSigner();

// The wallet the caller signs with, from the wallet created in step 2.
const walletAccount = walletAccounts[0];

const scope = TokenScope.BusinessAccountsigneradd;
const { isRequired } = await checkStepUpAuth({ scope });
if (isRequired) {
  await authenticatePasskeyMFA({ requestedScopes: [scope] });
}

const params = {
  businessAccountId: account.id,
  walletAccount,
  signerType: 'endUser' as const, // only 'endUser' is supported today
  targetIdentity: {
    identifier: 'teammate@acme.com',
    identifierType: 'email' as const,
  },
};

let result;
try {
  result = await addBusinessAccountSigner(params);
} catch (error) {
  if (!(error instanceof BusinessAccountIntentRequiredError)) {
    throw error;
  }
  const { intent, intentSignature } = await signBusinessAccountIntent({
    intent: error.intent,
  });
  result = await addBusinessAccountSigner({
    ...params,
    intent,
    intentSignature,
  });
}

if (isBusinessAccountActionRequired(result)) {
  // Quorum not met: show the proposal for approval before the reshare runs.
  console.log('Approval required', result.proposalId);
} else {
  // result.shareSetId → use it to set an initial policy for the signer.
  console.log('Signer added', result.shareSetId);
}
```

A user who signed in with an email code has no passkey, so `authenticatePasskeyMFA` throws `NoPasskeyCredentialsFoundError`. Replace that call with an email code check that passes the same `requestedScopes`:

```tsx theme={"system"}
import { useSendEmailOTP, useVerifyOTP } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: sendEmailOTP } = useSendEmailOTP();
const { mutateAsync: verifyOTP } = useVerifyOTP();

if (isRequired) {
  const email = 'owner@acme.com'; // the signed-in user's own email
  const { verificationUUID } = await sendEmailOTP({ email });

  // `code` is the code the user receives by email.
  await verifyOTP({
    otpVerification: { email, verificationUUID },
    verificationToken: code,
    requestedScopes: [scope],
  });
}
```

See [Step-up authentication](https://docs.dynamic.xyz/docs/javascript/building-ui/step-up-authentication.md) for the full email code flow and the other methods.

Expected outcome: the teammate's email resolves to a user, the reshare mints their key share, and the call resolves to `{ shareSetId }` (or a pending proposal when governance applies). Becoming a signer also makes the teammate a `viewer` member of the account. See [Manage signers](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/manage-signers.md) for `password` and `targetSignerPassword`, which are required when your environment mandates wallet passwords.

**4. Give the teammate an admin role**

A signer can sign, but only a **member** can approve proposals or administer the account. Step 3 already made the teammate a `viewer` member, and adding a member again returns the existing member and ignores `role`. Change the role with `updateBusinessAccountMemberRole` instead, scoped to `business_account:member:role:update`. It needs the member's `userId`, which `getBusinessAccount` returns.

```tsx theme={"system"}
import {
  authenticatePasskeyMFA,
  checkStepUpAuth,
  TokenScope,
} from '@dynamic-labs-sdk/client';
import { getBusinessAccount } from '@dynamic-labs-sdk/client/waas';
import { useUpdateBusinessAccountMemberRole } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: updateBusinessAccountMemberRole } =
  useUpdateBusinessAccountMemberRole();

const scope = TokenScope.BusinessAccountmemberroleupdate;
const { isRequired } = await checkStepUpAuth({ scope });
if (isRequired) {
  await authenticatePasskeyMFA({ requestedScopes: [scope] });
}

// Besides you, the owner, the only member is the teammate step 3 made a viewer.
const detail = await getBusinessAccount({ businessAccountId: account.id });
const teammate = detail.members.find(({ role }) => role === 'viewer');
if (!teammate) {
  throw new Error('Teammate is not a member yet');
}

await updateBusinessAccountMemberRole({
  businessAccountId: account.id,
  userId: teammate.userId,
  role: 'admin',
});
```

For a user without a passkey, use the email code version from the previous step, with `scope` set to `TokenScope.BusinessAccountmemberroleupdate`.

Expected outcome: the teammate is now an `admin` member of the account and still a signer on its wallet.

**5. Require an approval before signing**

`createPolicy` attaches rules to the account. An `approvalRequirements` rule turns signing into a propose, approve, and resume flow. This rule requires one approval from an `admin` or `owner` member for every signing operation on the account's EVM wallets.

```tsx theme={"system"}
import { WaasChainEnum } from '@dynamic-labs-sdk/client';
import { useCreatePolicy } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: createPolicy } = useCreatePolicy();

await createPolicy({
  scope: { businessAccountId: account.id },
  chain: WaasChainEnum.Evm,
  rules: {
    approvalRequirements: [
      {
        v: 1,
        id: 'sign-with-approval',
        eligibleApproverRoles: ['admin', 'owner'],
        requiredApprovals: 1,
      },
    ],
  },
});
```

Expected outcome: the account-layer policy now requires one approval from an `admin` or `owner` before the wallet signs. Set `eligibleApproverRoles` explicitly; omitting it lets any verified approver role satisfy the requirement, and the initiator never counts toward `requiredApprovals`. The rule needs more eligible members than `requiredApprovals`, so `['admin']` alone would be refused with `governance_unsatisfiable` on an account with one admin; see [Requirements and roles](https://docs.dynamic.xyz/docs/business-accounts/approvals.md#requirements-and-roles).

`createPolicy` writes legacy rules. To require approval only under a specific condition, such as a large transfer of one token, write a declarative approval rule instead; see [Declarative rules for business accounts](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/policies/declarative-rules.md).

**6. Sign a message through the quorum**

With the quorum rule active, `signMessage` rejects with `SigningConsentRequiredError` instead of returning a signature: the SDK signs the operation's intent automatically and opens a proposal carrying that intent.

```tsx theme={"system"}
import { signMessage } from '@dynamic-labs-sdk/client';
import { isSigningConsentRequiredError } from '@dynamic-labs-sdk/client/waas';
import { useGetWalletAccounts } from '@dynamic-labs-sdk/react-hooks';

const { data: walletAccounts = [] } = useGetWalletAccounts();
const walletAccount = walletAccounts[0];

let proposalId: string | undefined;
try {
  const { signature } = await signMessage({
    walletAccount,
    message: 'gm from Acme Treasury',
  });
} catch (error) {
  if (!isSigningConsentRequiredError(error)) {
    throw error;
  }
  // APPROVALS_REQUIRED: a proposal opened and needs another member.
  proposalId = error.proposalId;
}
```

The teammate then approves from their own session, reading the proposals awaiting their approval. Approving needs an elevated token scoped to `business_account:proposal:approve`, so the teammate steps up first. The code below uses an email code. A teammate with a passkey calls `authenticatePasskeyMFA` with the same `requestedScopes`, as in step 3.

```tsx theme={"system"}
import { checkStepUpAuth, TokenScope } from '@dynamic-labs-sdk/client';
import {
  useApproveBusinessAccountProposal,
  useListBusinessAccountProposalsAwaitingMyApproval,
  useSendEmailOTP,
  useVerifyOTP,
} from '@dynamic-labs-sdk/react-hooks';

const { data: awaiting = [] } =
  useListBusinessAccountProposalsAwaitingMyApproval();
const { mutateAsync: approve } = useApproveBusinessAccountProposal();
const { mutateAsync: sendEmailOTP } = useSendEmailOTP();
const { mutateAsync: verifyOTP } = useVerifyOTP();

const proposal = awaiting.find(({ id }) => id === proposalId);
if (!proposal) {
  throw new Error('Proposal not found');
}

const scope = TokenScope.BusinessAccountproposalapprove;
const { isRequired } = await checkStepUpAuth({ scope });
if (isRequired) {
  const email = 'teammate@acme.com'; // the teammate's own email
  const { verificationUUID } = await sendEmailOTP({ email });

  // `code` is the code the teammate receives by email.
  await verifyOTP({
    otpVerification: { email, verificationUUID },
    verificationToken: code,
    requestedScopes: [scope],
  });
}

await approve({
  businessAccountId: proposal.businessAccountId,
  proposal,
});
```

Back in the initiator's session, `resumeSigningProposal` consumes the satisfied proposal and returns the signature.

```tsx theme={"system"}
import { useListBusinessAccountProposals } from '@dynamic-labs-sdk/react-hooks';

const { data: pending = [] } = useListBusinessAccountProposals({
  businessAccountId: account.id,
  status: 'pending',
});

const proposal = pending.find(({ id }) => id === proposalId);
if (!proposal) {
  throw new Error('Proposal not found');
}
```

```tsx theme={"system"}
import { useResumeSigningProposal } from '@dynamic-labs-sdk/react-hooks';

const { mutateAsync: resume } = useResumeSigningProposal();

const { signature } = await resume({ proposal, walletAccount });
```

Expected outcome: `signature` is a hex signature from the business-account wallet, produced only after the admin approved the proposal. For the full quorum flow, including intent inspection and transaction signing, see [Quorum policies](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/policies/quorum-policies.md).

## What just happened

You created a business account with yourself as owner, gave it an EVM wallet on which you are the first signer, elevated your session for the `business_account:signer:add` and `business_account:member:role:update` scopes, added a teammate as a second signer (signing the server-returned intent when the target was identified by email), made them an `admin` member, attached a quorum rule requiring an admin approval to sign, had the teammate step up for `business_account:proposal:approve` to approve, and produced a signature through the propose, approve, and resume flow. The account, not any single user, owns the wallet and its signing rights.

## Next steps

> [card] [Members & roles](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/members-and-roles.md)
>
> Change roles, remove members, or invite viewers.

> [card] [Governance](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/governance.md)
>
> Require approvals for signer, member, and wallet changes.

> [card] [Quorum policies](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/policies/quorum-policies.md)
>
> The full propose, approve, and resume signing flow, including transactions.

> [card] [Policies](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/policies/overview.md)
>
> Constrain what signers and wallets may do.

> [card] [Step-up auth](https://docs.dynamic.xyz/docs/javascript/reference/business-accounts/step-up-auth.md)
>
> Every scope that requires an elevated token, and every way to get one.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.