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

# Use a Quorum Signer

> Create an m-of-n quorum signer and approve transactions with several members

A quorum signer approves a wallet action only when `threshold` of its members sign. This guide uses the REST API to create a wallet with a 2-of-3 recovery quorum, add a 2-of-3 operational quorum, sign a transaction with it, and rotate quorum members. For the concept, see [Quorum Signers](/wallets/concepts/quorum-signers).

The examples use an EVM wallet on `base-sepolia` with two quorums: a recovery quorum of email and phone members, and an operational quorum of three external wallets that signs the transaction. The same requests work on Solana and Stellar, with the Solana difference shown in [Sign a Transaction with a Quorum](#sign-a-transaction-with-a-quorum).

## Prerequisites

<Snippet file="enterprise-feature.mdx" />

* **API key**: A server API key with the `wallets.create`, `wallets:transactions.create`, and `wallets:transactions.sign` scopes. In staging, all scopes are included by default.
* **Members**: The signers that will form the quorum, from 2 to 10. Each member must be able to sign its own approval message. See [Rules and Limits](/wallets/concepts/quorum-signers#rules-and-limits) for the member types on each chain.

## Create a Wallet with a Quorum Recovery Method

Add an entry with `type: "quorum"` to `recoveryMethods`. Set `threshold` to the number of members that must sign, and list the members in `members`. Here, any 2 of the 3 members can recover the wallet and change its signers.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
      --url https://staging.crossmint.com/api/2025-06-09/wallets \
      --header 'Content-Type: application/json' \
      --header 'X-API-KEY: YOUR_SERVER_API_KEY' \
      --data '{
          "chainType": "evm",
          "config": {
              "recoveryMethods": [
                  {
                      "type": "quorum",
                      "threshold": 2,
                      "members": [
                          { "type": "email", "email": "alice@example.com" },
                          { "type": "email", "email": "bob@example.com" },
                          { "type": "phone", "phone": "+12025550143" }
                      ]
                  }
              ]
          }
      }'
  ```

  ```js Node.js theme={null}
  const url = "https://staging.crossmint.com/api/2025-06-09/wallets";

  const payload = {
      chainType: "evm",
      config: {
          recoveryMethods: [
              {
                  type: "quorum",
                  threshold: 2,
                  members: [
                      { type: "email", email: "alice@example.com" },
                      { type: "email", email: "bob@example.com" },
                      { type: "phone", phone: "+12025550143" },
                  ],
              },
          ],
      },
  };

  const response = await fetch(url, {
      method: "POST",
      headers: {
          "X-API-KEY": "YOUR_SERVER_API_KEY",
          "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
  });

  const data = await response.json();
  console.log(data.config.recoveryMethods);
  ```

  ```python Python theme={null}
  import requests

  url = "https://staging.crossmint.com/api/2025-06-09/wallets"

  payload = {
      "chainType": "evm",
      "config": {
          "recoveryMethods": [
              {
                  "type": "quorum",
                  "threshold": 2,
                  "members": [
                      {"type": "email", "email": "alice@example.com"},
                      {"type": "email", "email": "bob@example.com"},
                      {"type": "phone", "phone": "+12025550143"}
                  ]
              }
          ]
      }
  }
  headers = {
      "X-API-KEY": "YOUR_SERVER_API_KEY",
      "Content-Type": "application/json"
  }

  response = requests.post(url, json=payload, headers=headers)
  print(response.json()["config"]["recoveryMethods"])
  ```
</CodeGroup>

The response lists the quorum under `config.recoveryMethods` with the `locator` that Crossmint assigned to it. Each member also has its own locator:

```json theme={null}
[
    {
        "type": "quorum",
        "locator": "quorum:3f6b2c1e-8a4d-4f7b-9c2e-5d1a7b8e9f04",
        "threshold": 2,
        "members": [
            { "type": "email", "email": "alice@example.com", "locator": "email:alice@example.com" },
            { "type": "email", "email": "bob@example.com", "locator": "email:bob@example.com" },
            { "type": "phone", "phone": "+12025550143", "locator": "phone:+12025550143" }
        ]
    }
]
```

A quorum can also sit next to other recovery methods in the same list. Each other method then acts alone, and the quorum is a second path that needs `threshold` members.

## Add a Quorum as an Operational Signer

To require several approvals for day-to-day transactions, register the quorum as an operational signer with the [register delegated key](/api-reference/wallets/register-delegated-key) endpoint. The `signer` field takes the same quorum object:

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
      --url https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/signers \
      --header 'Content-Type: application/json' \
      --header 'X-API-KEY: YOUR_SERVER_API_KEY' \
      --data '{
          "chain": "base-sepolia",
          "signer": {
              "type": "quorum",
              "threshold": 2,
              "members": [
                  { "type": "external-wallet", "address": "0x8617E340B3D01FA5F11F306F4090FD50E238070D" },
                  { "type": "external-wallet", "address": "0xde709f2102306220921060314715629080e2fb77" },
                  { "type": "external-wallet", "address": "0x27b1fdb04752bbc536007a920d24acb045561c26" }
              ]
          }
      }'
  ```

  ```js Node.js theme={null}
  const url = "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/signers";

  const payload = {
      chain: "base-sepolia",
      signer: {
          type: "quorum",
          threshold: 2,
          members: [
              { type: "external-wallet", address: "0x8617E340B3D01FA5F11F306F4090FD50E238070D" },
              { type: "external-wallet", address: "0xde709f2102306220921060314715629080e2fb77" },
              { type: "external-wallet", address: "0x27b1fdb04752bbc536007a920d24acb045561c26" },
          ],
      },
  };

  const response = await fetch(url, {
      method: "POST",
      headers: {
          "X-API-KEY": "YOUR_SERVER_API_KEY",
          "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
  });

  const data = await response.json();
  console.log(data);
  ```

  ```python Python theme={null}
  import requests

  url = "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/signers"

  payload = {
      "chain": "base-sepolia",
      "signer": {
          "type": "quorum",
          "threshold": 2,
          "members": [
              {"type": "external-wallet", "address": "0x8617E340B3D01FA5F11F306F4090FD50E238070D"},
              {"type": "external-wallet", "address": "0xde709f2102306220921060314715629080e2fb77"},
              {"type": "external-wallet", "address": "0x27b1fdb04752bbc536007a920d24acb045561c26"}
          ]
      }
  }
  headers = {
      "X-API-KEY": "YOUR_SERVER_API_KEY",
      "Content-Type": "application/json"
  }

  response = requests.post(url, json=payload, headers=headers)
  print(response.json())
  ```
</CodeGroup>

A recovery method of the wallet approves the registration, as for any other signer — see [Add Signers](/wallets/guides/signers/add-signers). If that recovery method is a quorum, `threshold` of its members approve, as in the next section. The quorum can act only after its registration succeeds. Its `locator`, for example `quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51`, is listed with the wallet's signers.

## Sign a Transaction with a Quorum

These steps use the operational quorum from the previous section. Its members are external wallets, which sign approvals with their own keys.

<Steps>
  <Step title="Create the transaction">
    Call the [create transaction](/api-reference/wallets/create-transaction) endpoint and set `params.signer` to the quorum's locator. A member's locator does not act for the quorum: members approve, they never create the transaction.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --request POST \
          --url https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions \
          --header 'Content-Type: application/json' \
          --header 'X-API-KEY: YOUR_SERVER_API_KEY' \
          --data '{
              "params": {
                  "calls": [{
                      "to": "0x52908400098527886E0F7030069857D2E4169EE7",
                      "value": "0x2386f26fc10000",
                      "data": "0x"
                  }],
                  "chain": "base-sepolia",
                  "signer": "quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51"
              }
          }'
      ```

      ```js Node.js theme={null}
      const url = "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions";

      const payload = {
          params: {
              calls: [
                  {
                      to: "0x52908400098527886E0F7030069857D2E4169EE7",
                      value: "0x2386f26fc10000",
                      data: "0x",
                  },
              ],
              chain: "base-sepolia",
              signer: "quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51",
          },
      };

      const response = await fetch(url, {
          method: "POST",
          headers: {
              "X-API-KEY": "YOUR_SERVER_API_KEY",
              "Content-Type": "application/json",
          },
          body: JSON.stringify(payload),
      });

      const transaction = await response.json();
      console.log(transaction.id, transaction.approvals.pending);
      ```

      ```python Python theme={null}
      import requests

      url = "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions"

      payload = {
          "params": {
              "calls": [{
                  "to": "0x52908400098527886E0F7030069857D2E4169EE7",
                  "value": "0x2386f26fc10000",
                  "data": "0x"
              }],
              "chain": "base-sepolia",
              "signer": "quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51"
          }
      }
      headers = {
          "X-API-KEY": "YOUR_SERVER_API_KEY",
          "Content-Type": "application/json"
      }

      response = requests.post(url, json=payload, headers=headers)
      transaction = response.json()
      print(transaction["id"], transaction["approvals"]["pending"])
      ```
    </CodeGroup>

    On Solana, also set `approvingMembers` to exactly `threshold` member locators. Only those members can approve:

    ```json theme={null}
    {
        "params": {
            "transaction": "<base58-serialized-transaction>",
            "signer": "quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51",
            "approvingMembers": [
                "external-wallet:GbA2NZfpAnRVM2G2BG29qooqsYbdV5c2WVFymJ8MMir7",
                "external-wallet:7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV"
            ]
        }
    }
    ```

    On EVM and Stellar, signature requests work the same way: set `signer` to the quorum's locator.
  </Step>

  <Step title="Sign each member's message">
    The response has one pending approval entry for the quorum. Its `quorumApprovals` object lists each member that can approve, with the `message` that member must sign. On EVM and Stellar, all members are listed. On Solana, only the members in `approvingMembers` are listed.

    ```json theme={null}
    "approvals": {
        "pending": [
            {
                "signer": { "type": "quorum", "locator": "quorum:9d41a7c2-5e3b-4c8f-a1d6-2b7e0f3c8a51" },
                "quorumApprovals": {
                    "threshold": 2,
                    "remaining": 2,
                    "pending": [
                        { "member": { "type": "external-wallet", "locator": "external-wallet:0x8617E340B3D01FA5F11F306F4090FD50E238070D" }, "message": "0x7b2a..." },
                        { "member": { "type": "external-wallet", "locator": "external-wallet:0xde709f2102306220921060314715629080e2fb77" }, "message": "0x7b2a..." },
                        { "member": { "type": "external-wallet", "locator": "external-wallet:0x27b1fdb04752bbc536007a920d24acb045561c26" }, "message": "0x7b2a..." }
                    ],
                    "submitted": []
                }
            }
        ],
        "submitted": []
    }
    ```

    Each member signs its own `message` with its own key. Members sign independently and in any order. The messages can be different for each member type, so always sign the `message` from that member's entry.
  </Step>

  <Step title="Submit the member approvals">
    Call the [approve transaction](/api-reference/wallets/approve-transaction) endpoint. Set `approvals[].signer` to the member's own locator, never to the quorum's locator. You can submit the members together, as below, or in separate requests as each member signs.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --request POST \
          --url https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions/<transaction-id>/approvals \
          --header 'Content-Type: application/json' \
          --header 'X-API-KEY: YOUR_SERVER_API_KEY' \
          --data '{
              "approvals": [
                  {
                      "signer": "external-wallet:0x8617E340B3D01FA5F11F306F4090FD50E238070D",
                      "signature": "<first-member-signature>"
                  },
                  {
                      "signer": "external-wallet:0x27b1fdb04752bbc536007a920d24acb045561c26",
                      "signature": "<second-member-signature>"
                  }
              ]
          }'
      ```

      ```js Node.js theme={null}
      const url =
          "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions/<transaction-id>/approvals";

      const payload = {
          approvals: [
              {
                  signer: "external-wallet:0x8617E340B3D01FA5F11F306F4090FD50E238070D",
                  signature: "<first-member-signature>",
              },
              {
                  signer: "external-wallet:0x27b1fdb04752bbc536007a920d24acb045561c26",
                  signature: "<second-member-signature>",
              },
          ],
      };

      const response = await fetch(url, {
          method: "POST",
          headers: {
              "X-API-KEY": "YOUR_SERVER_API_KEY",
              "Content-Type": "application/json",
          },
          body: JSON.stringify(payload),
      });

      const transaction = await response.json();
      console.log(transaction.status);
      ```

      ```python Python theme={null}
      import requests

      url = "https://staging.crossmint.com/api/2025-06-09/wallets/<wallet-address>/transactions/<transaction-id>/approvals"

      payload = {
          "approvals": [
              {
                  "signer": "external-wallet:0x8617E340B3D01FA5F11F306F4090FD50E238070D",
                  "signature": "<first-member-signature>"
              },
              {
                  "signer": "external-wallet:0x27b1fdb04752bbc536007a920d24acb045561c26",
                  "signature": "<second-member-signature>"
              }
          ]
      }
      headers = {
          "X-API-KEY": "YOUR_SERVER_API_KEY",
          "Content-Type": "application/json"
      }

      response = requests.post(url, json=payload, headers=headers)
      print(response.json()["status"])
      ```
    </CodeGroup>

    After each approval, `quorumApprovals.remaining` goes down by 1. If a member submits the same approval again, the request succeeds and changes nothing.
  </Step>

  <Step title="Check the result">
    When `remaining` reaches 0, the quorum entry moves to `approvals.submitted` and Crossmint broadcasts the transaction. Poll the [get transaction](/api-reference/wallets/get-transaction) endpoint until `status` is `success` or `failed`.
  </Step>
</Steps>

## Rotate a Quorum

You cannot change the members or the threshold of a quorum. To rotate them, add a new quorum and then remove the old one. The new quorum gets a new locator, and it can keep some of the old quorum's members.

### Recovery Quorum

1. Add the new quorum with the [add recovery method](/api-reference/wallets/add-recovery-method) endpoint.
2. Remove the old quorum by its locator with the [remove recovery method](/api-reference/wallets/remove-recovery-method) endpoint.

Add the new quorum first: a wallet must always keep at least one recovery method. See [Manage Recovery Methods](/wallets/guides/recovery/manage-recovery-methods) for the full request shapes.

### Operational Quorum

1. Register the new quorum with the [register delegated key](/api-reference/wallets/register-delegated-key) endpoint.
2. Remove the old quorum by its locator with the [remove signer](/api-reference/wallets/remove-delegated-key) endpoint. See [Remove a Signer](/wallets/guides/signers/remove-signer).

<Warning>
  When the old quorum is removed, its transactions that are waiting for approval fail. Submit or cancel them before you rotate, or create them again with the new quorum.
</Warning>

## Next Steps

<CardGroup cols={3}>
  <Card title="Quorum Signers" icon="users" href="/wallets/concepts/quorum-signers">
    Understand how m-of-n approval works on each chain
  </Card>

  <Card title="Manage Recovery Methods" icon="key" href="/wallets/guides/recovery/manage-recovery-methods">
    Add or remove recovery methods on an existing wallet
  </Card>

  <Card title="Approve Transaction" icon="code" href="/api-reference/wallets/approve-transaction">
    Submit member approvals in the API playground
  </Card>
</CardGroup>
