Set up self-custody wallets
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.
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:
-
Send the
POST /platform/v2/agreementsrequest with anIdempotency-Keyheader 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 toSELF_CUSTODY.customerType: The type of customer being activated. The following example usesINDIVIDUAL.countryCode: The customer’s country code.model: Set toEMBEDDED. 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 toRELIANCEby 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/agreementsto the customer and collect their response. After the customer accepts the agreement, record their decision usingPOST /platform/v2/agreements/actionsas described in step 3. -
Send the
GET /platform/v2/customers/{customerId}/agreementsrequest 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.
-
After the customer accepts the Self-Custody Agreement, send the
POST /platform/v2/agreements/actionsrequest with theACCEPTaction and anIdempotency-Keyheader. Pass the samereferenceyou 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:
-
Send the
GET /ledger/v2/wallets/profilesrequest to find theidof a self-custody wallet profile to use asprofileId.Identify the profile with an
idin the formatcrypto: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
profileIdwhen creating additional self-custody wallets. -
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.
-
Send the
POST /ledger/v2/walletsrequest with anIdempotency-Keyheader 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
INACTIVEand becomesACTIVEonce 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"} -
Wait for the wallet to become active. A newly created wallet is initially returned with a status of
INACTIVEwhile provisioning completes.warningThe 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
paymentInstrumentsarray 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"}
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.
{
"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.
- Send payouts from a self-custody wallet.
- Learn how BVNK protects key material guide.
- Export a customer's key so they can manage or recover funds outside BVNK.
- Manage a customer’s key pair.
- Review the partner and customer responsibilities for handling key material.
- View the Self-custody key management API reference for detailed endpoint documentation.