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

# React Native Business Accounts

> Use business accounts in a React Native app: shared wallets, signers, and the elevated tokens that mutations require on device.

A business account groups wallets, signers, and members under one account, so a team shares custody of an embedded wallet instead of one person holding it. React Native uses the same functions and hooks as the web, from `@dynamic-labs-sdk/client/waas` and `@dynamic-labs-sdk/react-hooks`. This page covers the parts that behave differently on device: the WaaS host origin, where the key share lives, and how to obtain an elevated token without a browser popup.

<Note>
  Before this: create and initialize a Dynamic client (see [Creating a Dynamic Client](/docs/javascript/reference/client/create-dynamic-client), [Initializing the Dynamic Client](/docs/javascript/reference/client/initialize-dynamic-client)) and finish [React Native Setup](/docs/javascript/react-native/setup). For the account model (owners, admins, viewers, signers) and the full API, see [Business Accounts](/docs/business-accounts/accounts).
</Note>

<Note>
  Business Accounts are in **early access**. [Talk to us](https://www.dynamic.xyz/talk-to-us) if you'd like to participate.
</Note>

## What you need on React Native

* An embedded wallet extension registered on the client, for example `addWaasEvmExtension`. See [Adding Extensions](/docs/javascript/reference/adding-extensions).
* `metadata.universalLink` set to a domain you control, and that domain allowlisted as an origin in your environment settings. On device there is no `window.location.origin`, so the SDK sends your universal link as the host origin of the hosted signer. If it is missing or not allowlisted, the signer refuses to load and wallet operations fail.
* `react-native-keychain` installed and the native app rebuilt, from [Expo](/docs/javascript/react-native/expo) or [Bare React Native](/docs/javascript/react-native/bare-react-native) setup.

```ts dynamicClient.ts theme={"system"}
import { createDynamicClient } from '@dynamic-labs-sdk/client';

export const dynamicClient = createDynamicClient({
  environmentId: 'YOUR_ENVIRONMENT_ID',
  metadata: {
    name: 'My App',
    nativeLink: 'myapp://',
    // Sent as the host origin of the hosted signer, so it must be allowlisted
    universalLink: 'https://yourdomain.com',
  },
});
```

<Info>
  The signer's key share is stored in the device keychain, readable only while the device is unlocked and scoped to that device. It is excluded from iCloud Keychain sync and does not travel to a new device on an encrypted-backup restore, so a signer who changes device needs a fresh share. Add them again with `addBusinessAccountSigner` on the new device, or use the wallet's recovery flow.
</Info>

## Create an account and a wallet

`createBusinessAccount` makes the caller the owner. `createWalletForBusinessAccount` then creates an embedded wallet owned by the account, on the chain you pass.

<Tabs>
  <Tab title="JavaScript">
    ```ts theme={"system"}
    import {
      createBusinessAccount,
      createWalletForBusinessAccount,
    } from '@dynamic-labs-sdk/client/waas';

    const account = await createBusinessAccount({
      name: 'Acme Treasury',
      externalRef: 'org_acme_123',
    });

    const { accountAddress } = await createWalletForBusinessAccount({
      businessAccountId: account.id,
      chain: 'EVM',
    });
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import {
      useCreateBusinessAccount,
      useCreateWalletForBusinessAccount,
    } from '@dynamic-labs-sdk/react-hooks';

    const CreateTreasury = () => {
      const { mutateAsync: createBusinessAccount } = useCreateBusinessAccount();
      const { mutateAsync: createWallet } = useCreateWalletForBusinessAccount();

      const onPress = async () => {
        const account = await createBusinessAccount({ name: 'Acme Treasury' });
        await createWallet({ businessAccountId: account.id, chain: 'EVM' });
      };

      return <Button title="Create treasury" onPress={onPress} />;
    };
    ```
  </Tab>
</Tabs>

Neither call requires an elevated token. Both refresh authentication for you, so the new account and wallet appear in the session that follows.

## Mutations require an elevated token

Every operation that changes who can administer or sign for an account is protected server side by an elevated access token: a short lived JWT scoped to one action. The regular session token is not enough, and a missing or expired elevated token returns a `403`.

| Operation | Required scope |
| - | - |
| `addBusinessAccountSigner` | `business_account:signer:add` |
| `removeBusinessAccountSigner` | `business_account:signer:remove` |
| `addBusinessAccountMember` | `business_account:member:add` |
| `removeBusinessAccountMember` | `business_account:member:remove` |
| `updateBusinessAccountMemberRole` | `business_account:member:role:update` |
| `transferBusinessAccountOwnership` | `business_account:transfer_ownership` |
| `addWalletToBusinessAccount` | `business_account:link_wallet` |
| `removeBusinessAccountWallet` | `business_account:wallet:remove` |

These do not require an elevated token: `createBusinessAccount`, `getBusinessAccount`, `listBusinessAccounts`, and `createWalletForBusinessAccount`.

### Obtain the token on device

Web apps often step up in a popup. React Native has no popup, so use a verification the user can complete in the app and pass `requestedScopes` to it. The SDK stores the elevated token from the response and attaches it to the next call, so you never handle the token yourself.

* **Email or SMS OTP:** `verifyOTP` accepts `requestedScopes`, and the code is entered in your own UI. This works on Expo and bare React Native with no extra native modules.
* **Passkey or TOTP MFA:** `authenticatePasskeyMFA` and `authenticateTotpMfaDevice` accept `requestedScopes`. Passkeys need the native associations from [React Native Passkeys](/docs/javascript/react-native/passkeys) before the OS will use them.
* **Your own backend:** mint a scoped JWT and call `requestExternalAuthElevatedToken`. See [External auth step-up](/docs/javascript/authentication-methods/step-up-auth/external-auth).

`checkStepUpAuth` tells you whether a verification is needed. It returns `{ isRequired: false }` without a network call when a valid token for that scope is already in state.

```ts addSigner.ts theme={"system"}
import {
  checkStepUpAuth,
  sendEmailOTP,
  verifyOTP,
  TokenScope,
} from '@dynamic-labs-sdk/client';
import { addBusinessAccountSigner } from '@dynamic-labs-sdk/client/waas';
import type { WaasWalletAccount } from '@dynamic-labs-sdk/client/waas';

// promptForCode is your implementation: show an input and resolve with the code
export const addSigner = async ({
  businessAccountId,
  email,
  promptForCode,
  walletAccount,
}: {
  businessAccountId: string;
  email: string;
  promptForCode: () => Promise<string>;
  walletAccount: WaasWalletAccount;
}) => {
  const scope = TokenScope.BusinessAccountsigneradd;
  const { isRequired } = await checkStepUpAuth({ scope });

  if (isRequired) {
    const { verificationUUID } = await sendEmailOTP({ email });
    await verifyOTP({
      otpVerification: { email, verificationUUID },
      requestedScopes: [scope],
      verificationToken: await promptForCode(),
    });
  }

  const { shareSetId } = await addBusinessAccountSigner({
    businessAccountId,
    targetIdentity: { identifier: 'teammate@acme.com', identifierType: 'email' },
    walletAccount,
  });

  return shareSetId;
};
```

Adding a signer runs an MPC reshare, so the caller must already be an active signer on that wallet and the app must stay in the foreground until it resolves.

### Handle an expired token

Elevated tokens are short lived, so a user who leaves the app mid flow can come back to a `403`. Catch it and send them through verification again.

```ts theme={"system"}
import { APIError } from '@dynamic-labs-sdk/client/core';
import { removeBusinessAccountWallet } from '@dynamic-labs-sdk/client/waas';

try {
  await removeBusinessAccountWallet({ businessAccountId, walletId });
} catch (error) {
  if (error instanceof APIError && error.status === 403) {
    // Elevated token missing or expired, verify again and retry
  }
}
```

## Sign with a business-account wallet

A business-account wallet is a standard embedded wallet, so signing is the usual [`signMessage`](/docs/javascript/reference/wallets/sign-message) call. Wallets you can sign for come back from [`getWalletAccounts`](/docs/javascript/reference/wallets/get-wallet-accounts), and `businessAccountId` is set on the ones a business account owns.

<Tabs>
  <Tab title="JavaScript">
    ```ts theme={"system"}
    import {
      getWalletAccounts,
      isWaasWalletAccount,
      signMessage,
    } from '@dynamic-labs-sdk/client';
    import type { WaasWalletAccount } from '@dynamic-labs-sdk/client/waas';

    const businessWallets = getWalletAccounts().filter(
      (walletAccount): walletAccount is WaasWalletAccount =>
        isWaasWalletAccount({ walletAccount }) &&
        Boolean((walletAccount as WaasWalletAccount).businessAccountId),
    );

    const { signedMessage } = await signMessage({
      message: 'gm',
      walletAccount: businessWallets[0],
    });
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { signMessage } from '@dynamic-labs-sdk/client';
    import { useGetWalletAccounts } from '@dynamic-labs-sdk/react-hooks';

    const SignWithTreasury = () => {
      const { data: walletAccounts = [] } = useGetWalletAccounts();

      return (
        <Button
          title="Sign"
          onPress={() =>
            signMessage({ message: 'gm', walletAccount: walletAccounts[0] })
          }
        />
      );
    };
    ```
  </Tab>
</Tabs>

Only active signers can sign. Being an admin or a viewer does not grant signing access.

<Tip>
  Signing fails right after you create or link a wallet? The wallet reaches the session on the next authentication refresh, so read it back with `getWalletAccounts` before signing rather than reusing the address from the create call.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Business Accounts" icon="building" href="/docs/business-accounts/accounts">
    The account model: owners, admins, viewers, and signers.
  </Card>

  <Card title="Manage signers" icon="key" href="/docs/javascript/reference/business-accounts/manage-signers">
    Grant and revoke signing access on a wallet.
  </Card>

  <Card title="Members and roles" icon="users" href="/docs/javascript/reference/business-accounts/members-and-roles">
    Add admins and viewers, and transfer ownership.
  </Card>

  <Card title="Policies" icon="shield-check" href="/docs/javascript/reference/business-accounts/policies/overview">
    Constrain what account wallets and signers can do.
  </Card>
</CardGroup>


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