Skip to main content
BVNK

Set up self-custody wallets

Beta

Self-custody wallets are currently in beta and available to eligible BVNK customers. Some features may change as we continue to improve it. Please review the documentation and endpoints carefully, test everything in the sandbox, and reach out to the Solutions team if you need help.

This guide explains how to activate self-custody wallets for customers, then create their first wallet.

Self-custody wallets are available through BVNK's embedded integration and use the same wallet APIs as custodial wallets. Your platform activates and creates wallets for customers and can initiate payment requests on their behalf.

Before you begin​

Before setting up self-custody wallets for customers:

  • Your BVNK account must be enabled for self-custody.
  • You need API credentials with access to the required customer and wallet endpoints.
  • Subscribe to the Ledger wallet status change notification (v2) so your platform can detect when a wallet changes status.

Self-custody wallets are currently available only to customers created specifically for self-custody wallets, and these customers can hold only self-custody wallets. You cannot activate self-custody wallets for existing customers who use other BVNK products.

Prohibited jurisdictions

Self-custody wallets cannot be activated for customers in sanctioned jurisdictions.

Activate self-custody wallets​

Before you can create a self-custody wallet, the customer must accept the BVNK Self-Custody Agreement, confirming they understand they are responsible for their own assets.

To activate self-custody wallets, complete the following steps:

  1. Send the POST /platform/v2/agreements request with an Idempotency-Key header to assign the Self-Custody Agreement to the customer. Remember to specify the following fields:

    • reference: A unique identifier within your account. You reuse it when accepting the agreement.
    • useCase: Set to SELF_CUSTODY.
    • customerType: The type of customer being activated. The following example uses INDIVIDUAL.
    • countryCode: The customer’s country code.
    • model: Set to EMBEDDED. You can omit it if your account is enabled exclusively for the embedded model. If the account is enabled for the partner-managed onboarding, omitting the value, will set it to RELIANCE by default.
    • applicantEmail: The customer's email address.
    Request
    {
    "reference": "3f8c2a71-5d4e-4b9a-8e1f-6c7d2b9a0e45",
    "useCase": "SELF_CUSTODY",
    "customerType": "INDIVIDUAL",
    "countryCode": "US",
    "model": "EMBEDDED",
    "applicantEmail": "olivia.bennett@example.com"
    }

    The response includes the customerId. Store it for the next steps.

    Present the agreement URL returned by POST /platform/v2/agreements to the customer and collect their response. After the customer accepts the agreement, record their decision using POST /platform/v2/agreements/actions as described in step 3.

  2. Send the GET /platform/v2/customers/{customerId}/agreements request to retrieve the agreement assigned to the customer.

    Example Response
    {
    "totalElements": 1,
    "totalPages": 1,
    "content": [
    {
    "id": "59baa21b-06ea-4429-9c1c-e9a0179af484",
    "agreement": {
    "version": null,
    "title": "Self-Custody Agreement",
    "locale": null
    },
    "status": "PENDING",
    "respondedAt": null,
    "respondedToDocumentChecksum": null,
    "createdAt": "2026-10-05T11:57:48.782607Z",
    "updatedAt": "2026-10-05T11:57:48.782616Z"
    }
    ],
    "pageable": {
    "pageNumber": 0,
    "pageSize": 64
    },
    "hasNext": false
    }

The agreement remains PENDING until the customer accepts it. Use its id as the agreementId in the next step.

  1. After the customer accepts the Self-Custody Agreement, send the POST /platform/v2/agreements/actions request with the ACCEPT action and an Idempotency-Key header. Pass the same reference you used in step 1.

    Request
    {
    "reference": "3f8c2a71-5d4e-4b9a-8e1f-6c7d2b9a0e45",
    "actions": [
    {
    "agreementId": "59baa21b-06ea-4429-9c1c-e9a0179af484",
    "type": "ACCEPT"
    }
    ]
    }

When the agreement status is ACCEPTED, the customer is activated for self-custody wallets, and you can create a wallet.

Create a wallet​

Self-custody wallets use the same endpoints as custodial wallets, but require two additional fields: network and profileId. Key management is handled through a dedicated set of endpoints under /ledger/v2/key-pairs.

To create a self-custody wallet, complete these steps:

  1. Send the GET /ledger/v2/wallets/profiles request to find the id of a self-custody wallet profile to use as profileId.

    Identify the profile with an id in the format crypto:self-custodial:<id>, for example, crypto:self-custodial:16f910f5. Available profiles depend on your account and jurisdiction.

    Once you have identified the applicable self-custody profile, you can reuse its profileId when creating additional self-custody wallets.

  2. Optional. If the customer intends to use an existing mnemonic, import it before creating their first self-custody wallet. Otherwise, BVNK provisions a master key when the first wallet is created. Import is then blocked until the BVNK-created key is deleted, which takes 7 days.

  3. Send the POST /ledger/v2/wallets request with an Idempotency-Key header to create the wallet. Remember to specify the following fields:

    • profileId: The self-custody wallet profile retrieved in step 1. Using a self-custody profile ensures the wallet is self-custodial.
    • network: Each request creates one wallet for a single network. This field is mandatory.

    Creating the customer’s first self-custody wallet automatically provisions their master key if one does not already exist. You do not need to provide a key identifier.

    Request
    {
    "customerId": "d7344ac6-523b-49ef-9662-007433c8d526",
    "currency": "USDC",
    "name": "Olivia's USDC Ethereum wallet",
    "network": "ETHEREUM",
    "profileId": "crypto:self-custodial:16f910f5"
    }

    The response includes wallet details. The wallet is initially INACTIVE and becomes ACTIVE once activation is complete:

    Example Response
    {
    "id": "a:26081030868292:Lu1Gywh:1",
    "name": "USDC Ethereum",
    "customer": {
    "id": "d7344ac6-523b-49ef-9662-007433c8d526",
    "name": "Apex Trading Ltd"
    },
    "status": "INACTIVE",
    "balance": {
    "amount": 0.000000,
    "currency": "USDC"
    },
    "paymentInstruments": [
    {
    "address": "0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97",
    "network": "ETHEREUM",
    "tag": null
    }
    ],
    "createdAt": "2026-09-10T14:05:12.830Z",
    "updatedAt": "2026-09-10T14:05:12.830Z"
    }
  4. Wait for the wallet to become active. A newly created wallet is initially returned with a status of INACTIVE while provisioning completes.

    warning

    The response may include a blockchain address while the wallet is in this state. Do not share or use the address until the wallet is ACTIVE.

    After the wallet is activated, the paymentInstruments array will include the blockchain receive address.

    Use the wallet status change webhook to receive notifications when the wallet transitions to ACTIVE.

    Example Response
    {
    "id": "a:26081030868292:Lu1Gywh:1",
    "name": "Olivia's USDC Ethereum wallet",
    "customer": {
    "id": "d7344ac6-523b-49ef-9662-007433c8d526",
    "name": "Olivia Bennett"
    },
    "status": "INACTIVE",
    "balance": {
    "amount": 0.000000,
    "currency": "USDC"
    },
    "paymentInstruments": [
    {
    "address": "0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97",
    "network": "ETHEREUM",
    "tag": null
    }
    ],
    "createdAt": "2026-09-10T14:05:12.830Z",
    "updatedAt": "2026-09-10T14:05:12.830Z"
    }
Why one wallet per network

Custodial wallets display one wallet per currency and manage network aliases internally. Self-custody addresses are derived according to BIP44 (purpose, coin_type), so an address is shared only across networks with the same coin type. For example, Ethereum, Arbitrum, Base, Polygon, and Binance share one address, while Solana and Tron use a unique address. The network field ensures each request creates a single wallet, and the response always returns one wallet object.

Supporting multiple networks​

Each self-custody wallet is created for a specific currency and network combination.

For example, USDC on Ethereum and USDC on Polygon are separate wallets belonging to the same customer.

See Protecting key material for how addresses on all supported networks derive from the customer's master key, and Export for the derivation path of each network.

Get a wallet​

To retrieve a self-custody wallet, use the same GET /ledger/v2/wallets/{id} request as for custodial wallets.

The response structure is identical to wallet creation. Because self-custody maintains one wallet per currency and network, the response contains only that wallet and its paymentInstruments entry for the network address.

Example Response
{
"id": "a:26081030868292:Lu1Gywh:1",
"name": "USDC Ethereum",
"customer": {
"id": "d7344ac6-523b-49ef-9662-007433c8d526",
"name": "Apex Trading Ltd"
},
"status": "ACTIVE",
"balance": {
"amount": 128.450000,
"currency": "USDC"
},
"paymentInstruments": [
{
"address": "0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97",
"network": "ETHEREUM",
"tag": null
}
],
"createdAt": "2026-09-10T14:05:12.830Z",
"updatedAt": "2026-09-11T09:12:47.104Z"
}

To list all self-custody wallets a customer holds across networks, send a GET /ledger/v2/wallets request filtered by customerId.


Next steps

With self-custody wallets set up, you can manage the wallet lifecycle, secure customer keys, and initiate payments.

Was this page helpful?