> ## Documentation Index
> Fetch the complete documentation index at: https://sdk-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Organization

> Org context, team directory, membership, and employee wallet provisioning

## Overview

An organization is resolved from the API key. Every org method on `TrustodianSDK` reads the caller's active membership out of the key, and there is no `orgId` parameter anywhere in the org surface.

Organization support splits into four areas:

| Area | Covered on |
| - | - |
| Org context, team, policies, audit, address book | this page and [Governance & policies](https://sdk-guide.mintlify.site/sdk-guide/organization-policies.md) |
| Org-owned wallets and their key shares | [Organization Wallets](https://sdk-guide.mintlify.site/sdk-guide/organization-wallets.md) |
| Org-owned multisig vaults | [Organization Vaults](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md) |
| Orders against org vaults | [Organization Vaults](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md) and [Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md) |

> **[Organization Wallets](https://sdk-guide.mintlify.site/sdk-guide/organization-wallets.md)**
>
> Create, list, rename, and delete org wallets; portfolios, UTXOs, history, corporate address book

> **[Organization Vaults](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md)**
>
> Org multisig vault lifecycle per chain, and orders against org vaults

> **[Governance & Policies](https://sdk-guide.mintlify.site/sdk-guide/organization-policies.md)**
>
> Security policies, daily spending limits, limit groups, audit logs

> **[Key Management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md)**
>
> Key shares, `shareStore`, and the wallet-password session

## Roles and statuses

```typescript theme={null}
import { EOrgMemberRole, EOrgMemberStatus } from 'vaults-multichain-sdk';

enum EOrgMemberRole {
  MASTER = 'master',
  EMPLOYEE = 'employee',
}

enum EOrgMemberStatus {
  ACTIVE = 'active',
  PENDING = 'pending',
  DECLINED = 'declined',
  BLOCKED = 'blocked',
  REMOVED = 'removed',
}

type OrgMemberKycStatus = 'new' | 'pending' | 'verified' | 'failed' | 'expired';
```

Every org call checks, in order, that the API key is valid, that it resolves to an active member of the organization, that the member's role is allowed for that operation, and that the key carries the required permission. A role or permission mismatch is a `403` and surfaces as `PermissionDeniedError`.

> [!NOTE]
>
> A couple of operations require a passkey step-up for browser sessions, but for API-key callers the check defers to the key's own permissions. No step-up material is needed from the SDK.

## Reading the org context

```typescript theme={null}
const me = await sdk.getOrgMe();
// { organizationId: string; organizationName: string; orgMemberRole: EOrgMemberRole; memberId: string }

if (me.orgMemberRole === EOrgMemberRole.MASTER) {
  const { members } = await sdk.getOrgTeam();
}
```

`getOrgMe()` is allowed for both Master and Employee and needs no API-key permission at all, which makes it the cheapest probe to run on SDK init before any other org call. Its `organizationId` is the value the organization-scoped reads take, such as `listAllowances({ organizationId })` and `getStrictAuthStatus({ organizationId })`.

> [!WARNING]
>
> `getOrgMe()` resolves with `organizationId`, `organizationName`, `orgMemberRole` and `memberId`. An older backend leaves `organizationId` out. `orgMemberStatus` is typed as required but is not populated, so reading it yields `undefined`. Read a member's status from `getOrgTeam()` instead; a caller whose membership is no longer active is rejected with a `403` anyway.

## Team directory

```typescript theme={null}
interface OrgTeamMember {
  id: string;
  email: string;
  role: EOrgMemberRole;
  status: EOrgMemberStatus;
  userId: string | null;
  userName: string | null;
  kycStatus: OrgMemberKycStatus | null;
  assignedWalletsCount: number;
  assignedVaultsCount: number;
  invitedAt: string;
  joinedAt: string | null;
}

interface OrgTeamDirectoryResponse {
  members: OrgTeamMember[];
}

const { members } = await sdk.getOrgTeam();
```

`userId`, `userName`, `kycStatus` and `joinedAt` are `null` until the invitation is accepted. `member.id` is the `memberId` every other team method takes, not `userId`.

## Invitations

```typescript theme={null}
const invited = await sdk.inviteOrgMember({ email: 'teammate@example.com' });
// { memberId: string; email: string; status: string }
```

The server enforces the 10-user organization limit and the global one-organization-per-email rule; both come back as `ApiError` with status `409`.

> [!WARNING]
>
> `inviteToRegister({ email })` is not part of the published SDK API surface. Use `inviteOrgMember` for organization membership, and do not build a flow on `inviteToRegister` without confirming it against your deployment first.

## Removing a member

```typescript theme={null}
await sdk.removeOrgMember({ memberId }); // resolves void on HTTP 204
```

Removal runs one transaction that:

1. deletes every wallet assignment for the member,
2. deletes the member's server shares for those wallets (never the Master's own),
3. voids the member's signatures on pending org orders and recomputes the affected orders, writing one `Org Signature Voided` audit row per voided signature,
4. writes the `Org Member Removed` audit row,

then, post-commit, deletes the member's external MPC recovery shares and revokes their organization session.

## Wallet assignment

```typescript theme={null}
const { wallets } = await sdk.listOrgMemberWallets({ memberId });

const { assignmentId } = await sdk.assignOrgWallet({ memberId, walletId });

await sdk.unassignOrgWallet({ memberId, walletId }); // resolves void on HTTP 204
```

`walletId` may point at a wallet or a vault, and the backend validates that the asset belongs to the organization. A duplicate assignment returns `409`.

> [!NOTE]
>
> `unassignOrgWallet` is idempotent. Unassigning a wallet the member does not have returns `204`, not `404`. A `404` here means the *member* is not in this organization.

Revocation does more than delete a permission. The backend also removes the employee's key material for that wallet and retracts the pending-order signatures the member cast with it, recomputing every affected order. The member's session is *not* revoked, so they keep their other org access.

## Employee wallet provisioning

Assignment alone grants no cryptographic access. Signing capability moves through a three-step handshake in which neither party ever learns the other's wallet password, and no readable private key is ever transmitted.

```mermaid theme={null}
sequenceDiagram
    participant M as Master (SDK)
    participant S as Trustodian API
    participant E as Employee (SDK)

    M->>S: assign the wallet to the member
    Note over S: permission record only, no key material

    M->>M: rebuild the wallet key locally
    M->>S: hand over the key, encrypted to a one-time password
    M-->>E: the one-time password, out of band

    E->>S: fetch the handed-over material
    E->>E: decrypt, verify it produces the wallet's address
    E->>S: re-encrypt it under the employee's OWN password
    S-->>E: accepted
    E->>E: persist the employee's device share
```

**1. Assign (Master)**

```typescript theme={null}
await sdk.assignOrgWallet({ memberId, walletId });
```

Creates the permission record. `cryptoAccessStatus` on the member's wallet listing is `'none'`.

**2. Provision (Master)**

```typescript theme={null}
const masterUserRef = await sdk.getSdkUserId();

const { tempPassword, provisioned } = await sdk.provisionOrgMemberWalletAccess(
  masterUserRef,
  {
    memberId,
    walletId,
    address: wallet.address,
    chainType: EChainType.EVM,
    masterPassword,
    derivationSource: wallet.derivationSource,
    index: wallet.index,
    derivationSeedId: wallet.derivationSeedId,
  },
);
```

The call rebuilds the wallet key in the Master's process and hands it over encrypted to a one-time `tempPassword` bound to that member. Nothing readable reaches the server. Hand `tempPassword` to the employee over a channel you control. It is returned once and is not recoverable.

> [!WARNING]
>
> Pass `derivationSource` and `index` straight through from `listOrgMemberWallets()` / `listOrgWallets()`. A wallet with `derivationSource: 'seed'` is shareless: its key is re-derived from the Master's recovery phrase at `index`. Omitting these fields sends provisioning down the per-wallet share path, which hard-fails on a seed wallet. Pass `derivationSeedId` from the same listing too: it names the phrase the wallet derives from. Without it the SDK finds the phrase by matching the address.

**3. Accept (Employee)**

```typescript theme={null}
const employeeUserRef = await sdk.getSdkUserId();

const { accepted, version } = await sdk.acceptOrgWalletAccessWithPassword(
  employeeUserRef,
  {
    walletId,
    address: wallet.address,
    chainType: EChainType.EVM,
    memberId,
    tempPassword,
    ownPassword: employeeWalletPassword,
    passwordHint,
    createEncTokenIfMissing: true,
  },
);
```

The employee decrypts what the Master handed over, checks that the key really produces `address`, and then immediately re-encrypts it under their own wallet password, so the one-time password stops being useful the moment accept succeeds. The employee's device share is written to their `shareStore`. `createEncTokenIfMissing` defaults to `true`, so an employee with no wallet password yet has one established during accept.

> [!WARNING]
>
> If the accept succeeds server-side but persisting the device share fails, the SDK throws `"Access accepted on server but failed to save device share locally. Try accepting again."` The rotation already landed, so re-running accept with the same `tempPassword` is the recovery path.

**4. Revoke (Master)**

```typescript theme={null}
await sdk.unassignOrgWallet({ memberId, walletId }); // one wallet
await sdk.removeOrgMember({ memberId });             // the whole member
```

Server-side share cleanup plus signature retraction, as described above.

For splitting the provisioning handshake across processes, see [Lower-level building blocks](https://sdk-guide.mintlify.site/sdk-guide/organization-wallets.md#lower-level-building-blocks) on Organization Wallets.

> [!WARNING]
>
> This grants the wallet, not a seat in a Bitcoin or Cardano vault. Those vaults put each owner on a separate cosigner key that wallet provisioning does not cover, so an employee who needs to sign there takes a second grant. See [Vault keys for Bitcoin and Cardano](https://sdk-guide.mintlify.site/sdk-guide/organization-vaults.md#vault-keys-for-bitcoin-and-cardano).

### Crypto access status

The member wallet listing carries the state machine:

```typescript theme={null}
type OrgWalletCryptoAccessStatus = 'none' | 'pending' | 'accepted';

const { wallets } = await sdk.listOrgMemberWallets({ memberId });
const pending = wallets.filter((w) => w.cryptoAccessStatus === 'pending');
```

`'none'` means assigned only, `'pending'` means provisioned and awaiting accept, `'accepted'` means the employee holds usable shares.

## API-key permissions

Each operation requires any one of the listed permissions.

| Operation | Role | Permission (any of) |
| - | - | - |
| `getOrgMe` | Master, Employee | none |
| `getOrgTeam` | Master | `TeamManagement` |
| `inviteOrgMember` | Master | `TeamManagement` |
| `removeOrgMember` | Master | `TeamManagement` |
| `listOrgMemberWallets` | Master | `WalletManagement` |
| `assignOrgWallet` | Master | `WalletManagement` |
| `unassignOrgWallet` | Master | `WalletManagement` |
| `provisionOrgMemberWalletAccess` | Master | `WalletManagement`, `ViewBalances` |
| `acceptOrgWalletAccessWithPassword` | Employee only | `WalletManagement` |

> [!WARNING]
>
> Accepting a wallet handover is Employee-only, and a Master key calling it gets `403`. Provisioning
> is two-sided: it cannot be driven end to end from a single key.


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