Skip to content

CBTC Quick Start

⚠️ API Disclaimer. CBTC APIs have no formal versioning policy today. All endpoints and library interfaces described in this guide are subject to change. Breaking changes are communicated via the changelog. This disclaimer will be updated once a formal versioning and stability policy is established.

This step-by-step guide walks you through minting your first CBTC (wrapped Bitcoin) on the Canton Network. You will authenticate with Keycloak, create a deposit account, send BTC to a Taproot address, and receive 1:1 backed CBTC on your Canton participant node. The full process takes about 15 minutes of active work plus ~60 minutes of Bitcoin confirmation time.

🎯 What you will accomplish

  • Authenticate to the Canton Network via Keycloak
  • Create a CBTC deposit account
  • Obtain a Bitcoin deposit address
  • Send BTC and wait for confirmation
  • Verify your CBTC balance
  • Send CBTC to another party

🛠️ Install the SDK first. Every code example on this page uses the cbtc-lib Rust SDK. If you have not installed it yet, start with SDK Setup and Installation — it covers installing cbtc-lib and canton-lib, uploading the CBTC DAR files to your participant node, and configuring your environment variables. Come back here once the verification check on that page passes.


Before you begin minting wrapped Bitcoin on Canton, make sure you have the following:

Requirement Description
Canton participant node A running Canton participant node connected to the network. See Canton documentation for setup.
DA Registry Utility Installed and configured. See Digital Asset Utilities docs.
Keycloak credentials A valid Keycloak host, realm, client ID, username, and password for your environment.
Party ID Your Canton Party ID, obtained during onboarding.
Minter credential Required to mint or burn. Issued to your party by the CBTC registrar as part of onboarding. Request one via sales@bitsafe.finance. See Minting and Burning for how to check and accept it.
Rust toolchain If using cbtc-lib (Rust). Install via rustup.rs.
BTC to deposit Real BTC (mainnet) or testnet BTC (testnet). For testnet, you can use the CBTC Testnet Faucet to get test CBTC directly. For mainnet, you mint CBTC by depositing real BTC.

Choose Your CBTC Environment: Testnet or Mainnet

Section titled “Choose Your CBTC Environment: Testnet or Mainnet”

CBTC is available on three environments. Start with testnet for experimentation, then move to mainnet for production. CBTC also exists on Devnet, but minting and withdrawals are not available to external users on Devnet. You can use the faucet to obtain test CBTC for development.

🧪 Testnet vs. Mainnet: what is identical and what differs

  • Identical: DAR file, API surface, mint/burn flows, governance model, transfer mechanics
  • Differs: Attestor set (smaller on testnet), confirmation times (may be faster), Instrument IDs (different from mainnet), faucet-only BTC on testnet (no real value)
  • Mocked or unavailable on testnet: Real BTC settlement, production Attestor SLAs, mainnet fee structure
  • Operational note: Testnet may be reset without notice. Testnet CBTC balances and transaction history may not persist across resets. Do not rely on testnet state for production planning.
Variable Devnet Testnet Mainnet
REGISTRY_URL https://api.utilities.digitalasset-dev.com https://api.utilities.digitalasset-staging.com https://api.utilities.digitalasset.com
BITSAFE_API_URL https://api.devnet.bitsafe.finance https://api.testnet.bitsafe.finance https://api.mainnet.bitsafe.finance
DECENTRALIZED_PARTY_ID cbtc-network::12202a83c6f4082217c175e29bc53da5f2703ba2675778ab99217a5a881a949203ff cbtc-network::12201b1741b63e2494e4214cf0bedc3d5a224da53b3bf4d76dba468f8e97eb15508f cbtc-network::12205af3b949a04776fc48cdcc05a060f6bda2e470632935f375d1049a8546a3b262

Set these as environment variables before running any commands:

Terminal window
export BITSAFE_API_URL="https://api.testnet.bitsafe.finance"
export REGISTRY_URL="<your-registry-url>"
export CANTON_NETWORK="canton-testnet"
export DECENTRALIZED_PARTY_ID="<your-decentralized-party-id>"
export KEYCLOAK_HOST="https://your-keycloak-host"
export KEYCLOAK_REALM="your-realm"
export KEYCLOAK_CLIENT_ID="your-client-id"
export KEYCLOAK_USERNAME="your-username"
export KEYCLOAK_PASSWORD="your-password"
export LEDGER_HOST="https://your-ledger-host"
export PARTY_ID="your-party-id"

All CBTC operations require a valid Keycloak access token. The canton-lib crate provides a helper for this.

use keycloak::login::{password, token_url, PasswordParams};
let auth = password(PasswordParams {
client_id: keycloak_client_id.clone(),
username: keycloak_username.clone(),
password: keycloak_password.clone(),
url: token_url(&keycloak_host, &keycloak_realm),
}).await?;
let access_token = auth.access_token;
Terminal window
curl -X POST "${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=${KEYCLOAK_CLIENT_ID}" \
-d "username=${KEYCLOAK_USERNAME}" \
-d "password=${KEYCLOAK_PASSWORD}"

Save the access_token from the response. You will pass it as a Bearer token in all subsequent API calls.

💡 api_url in the code examples below is the BitSafe API base URL — the value you set as BITSAFE_API_URL above. It serves the /cbtc/v1/* endpoints that back deposit accounts, deposit addresses, and withdrawals.


A deposit account maps your Canton Party ID to a unique Bitcoin deposit address. You only need to create this once; the address can be reused for future deposits.

⚠️ This step requires a Minter credential. Deposit account creation fails without one. If you have not been issued a Minter credential yet, request one via sales@bitsafe.finance. See Minting and Burning for how to check for and accept your credential.

use cbtc::mint_redeem::{mint, attestor};
// First get account rules from the Attestor
let account_rules = attestor::get_account_contract_rules(&api_url).await?;
let deposit_account = mint::create_deposit_account(mint::CreateDepositAccountParams {
ledger_host: ledger_host.clone(),
party: party_id.clone(),
user_name: username.clone(),
access_token: access_token.clone(),
account_rules,
credential_cids: minter_credential_cids.clone(),
}).await?;

Submit a CreateDepositAccount command to the Canton Ledger API v2 endpoint. You can fetch the CBTCDepositAccountRules contract from an Attestor’s GET /cbtc/v1/account-contract-rules endpoint.

Terminal window
curl -X POST "${LEDGER_HOST}/v2/commands/submit-and-wait-for-transaction" \
--header 'Authorization: Bearer ${ACCESS_TOKEN}' \
--header 'Content-Type: application/json' \
--data '{
"commands": {
"commands": [
{
"ExerciseCommand": {
"templateId": "#cbtc:CBTC.DepositAccount:CBTCDepositAccountRules",
"contractId": "${DA_RULES_CID}",
"choice": "CBTCDepositAccountRules_CreateDepositAccount",
"choiceArgument": {
"owner": "${OWNER_PARTY}",
"credentialCids": ["${MINTER_CREDENTIAL_CID}"]
}
}
}
],
"actAs": [
"${OWNER_PARTY}"
],
"commandId": "someCommandID",
"disclosedContracts": [
{
"templateId": "#cbtc:CBTC.DepositAccount:CBTCDepositAccountRules",
"contractId": "${DA_RULES_CID}",
"createdEventBlob": "${DA_RULES_BLOB}",
"synchronizerId": ""
}
]
}
}'

ℹ️ Ephemeral contract IDs. DA_RULES_CID and DA_RULES_BLOB are ephemeral contract IDs. Query them from the Active Contract Service (POST /v2/state/active-contracts) at submit time. These IDs change after every consuming exercise.


Once the deposit account is created, retrieve the Bitcoin address associated with it. This address is derived from the Deposit Account’s id field (or the contract_id if the Deposit Account is new).

let btc_address = mint::get_bitcoin_address(mint::GetBitcoinAddressParams {
api_url: api_url.clone(),
account_id: deposit_account.account_id().to_string(),
}).await?;
println!("Send BTC to: {}", btc_address);

Retrieve the deposit address from:

GET ${BITSAFE_API_URL}/cbtc/v1/bitcoin-address/{accountId}

Use the deposit account’s id field as accountId.

Important: This address can be reused indefinitely for future deposits. You can also request additional deposit addresses if needed.


Send Bitcoin to the deposit address using any standard Bitcoin wallet or tooling.

⚠️ Deposits are subject to per-account limits. By default the minimum is 0.0001 BTC and the maximum is 5 BTC. A deposit outside that range will not be minted. Limits are set per account and can be adjusted on request - see Transaction Limits for how to read your account’s actual limits at runtime rather than assuming the defaults.

Bitcoin address: [your deposit address from Step 3]

After sending, you need to wait for 6 Bitcoin block confirmations before the Attestor network will process the deposit.


Step 5: Wait for Confirmations and Auto-Minting

Section titled “Step 5: Wait for Confirmations and Auto-Minting”

Once your BTC transaction reaches 6 confirmations:

  1. The Attestor network detects the confirmed deposit
  2. Each Attestor independently verifies the transaction
  3. When a threshold of Attestors confirm (via ConfirmDepositAction on the CBTC Governance module), CBTC is automatically minted to your Canton Party
  4. No further action is required from you This process typically takes 60 to 90 minutes, depending on Bitcoin block times.

use cbtc::active_contracts;
let holdings = active_contracts::get(active_contracts::Params {
ledger_host: ledger_host.clone(),
party: party_id.clone(),
access_token: access_token.clone(),
instrument_id: cbtc::InstrumentId {
admin: decentralized_party_id.clone(),
id: cbtc::CBTC_TICKER.to_string(),
},
// `None` reads every holding the party owns.
account: None,
}).await?;
println!("CBTC holdings: {} contract(s)", holdings.len());

Query active contracts filtered by the token holding interface:

Terminal window
curl -X POST "${LEDGER_HOST}/v2/state/active-contracts" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"eventFormat": {
"filtersByParty": {
"'"${PARTY_ID}"'": {
"cumulative": [{
"identifierFilter": {
"InterfaceFilter": {
"value": {
"interfaceId": "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
"includeInterfaceView": true,
"includeCreatedEventBlob": true
}
}
}
}]
}
},
"verbose": false
},
"activeAtOffset": '"${LATEST_OFFSET}"'
}'

💡 Filtering required. This query returns all token holdings, including Canton Coin (CC). To isolate your CBTC balance, you must filter the results client-side by the CBTC instrumentId. The cbtc-lib library handles this automatically via active_contracts::get. There is no single curl that can query and filter for CBTC holdings in one step. The activeAtOffset must be set to the actual latest offset from your ledger.

💡 UTXO model. CBTC uses a UTXO model similar to Bitcoin. Your balance may be spread across multiple holding contracts (soft limit of 10 UTXOs per party per token type). Use the cbtc::consolidate module to merge UTXOs, or cbtc::split to divide them.


Step 7: Transfer CBTC Between Canton Parties

Section titled “Step 7: Transfer CBTC Between Canton Parties”

A CBTC transfer has one of two outcomes. The registry creates a transfer offer, and the receiver accepts it. Or the registry settles the transfer on submission, and the receiver has nothing to accept. The registry settles on submission for a self transfer and for a receiver that pre-approved the sender. transfer::submit returns a TransferReceipt, and its outcome field tells you which outcome occurred.

use cbtc::transfer;
let receipt = transfer::submit(transfer::Params {
transfer: cbtc::Transfer {
sender: sender_party_id.clone(),
receiver: receiver_party_id.clone(),
amount: cbtc::DamlDecimal::parse("0.01")?,
instrument_id: cbtc::InstrumentId {
admin: decentralized_party_id.clone(),
id: "CBTC".to_string(),
},
requested_at: chrono::Utc::now().to_rfc3339(),
execute_before: (chrono::Utc::now() + chrono::Duration::hours(168)).to_rfc3339(),
input_holding_cids: None,
meta: None,
},
ledger_host: ledger_host.clone(),
access_token: access_token.clone(),
registry_url: registry_url.clone(),
decentralized_party_id: decentralized_party_id.clone(),
}).await?;

Do this step only when receipt.outcome is TransferOutcome::Pending. receipt.outcome.transfer_offer_cid() returns the offer ID. A Completed outcome leaves no offer to accept.

use cbtc::accept;
accept::submit(accept::Params {
transfer_offer_contract_id: transfer_contract_id.clone(),
receiver_party: receiver_party_id.clone(),
ledger_host: ledger_host.clone(),
access_token: receiver_access_token.clone(),
registry_url: registry_url.clone(),
decentralized_party_id: decentralized_party_id.clone(),
}).await?;

ℹ️ No curl example for transfers. The raw API transfer flow requires 5-6 sequential API calls with contract disclosures and is too complex to represent as a single curl example. Use cbtc-lib for transfers, or refer to the Canton Utility docs for the full API sequence.

💡 This is a free-of-payment (FOP) transfer. CBTC moves in one direction with nothing exchanged in return. If you need delivery-versus-payment — where CBTC only moves if a second leg moves atomically with it — use the cbtc::allocation module instead. See DvP Settlement Using Allocations in the Integration Guides.


Redeem CBTC: Convert Wrapped Bitcoin Back to BTC

Section titled “Redeem CBTC: Convert Wrapped Bitcoin Back to BTC”

To convert CBTC back to BTC (this also requires a Minter credential):

  1. Create a withdraw account specifying your Bitcoin destination address using the cbtc::mint_redeem::redeem module
  2. Submit the withdrawal which burns CBTC on Canton
  3. The Attestor network detects the burn and constructs a Bitcoin transaction
  4. Attestors sign the transaction via threshold signing (FROST)
  5. The BTC transaction is broadcast to the Bitcoin network
use cbtc::mint_redeem::redeem;
// 1. Create a withdraw account with your BTC destination address
let withdraw_account = redeem::create_withdraw_account(redeem::CreateWithdrawAccountParams {
ledger_host: ledger_host.clone(),
party: party_id.clone(),
user_name: username.clone(),
access_token: access_token.clone(),
account_rules_contract_id: rules.wa_rules.contract_id.clone(),
account_rules_template_id: rules.wa_rules.template_id.clone(),
account_rules_created_event_blob: rules.wa_rules.created_event_blob.clone(),
destination_btc_address: btc_destination_address.clone(),
credential_cids: minter_credential_cids.clone(),
}).await?;
// 2. Submit the withdrawal (burns CBTC, Attestors process BTC payout)
let updated_account = redeem::submit_withdraw(redeem::SubmitWithdrawParams {
ledger_host: ledger_host.clone(),
party: party_id.clone(),
user_name: username.clone(),
access_token: access_token.clone(),
api_url: api_url.clone(),
withdraw_account_contract_id: withdraw_account.contract_id.clone(),
amount: cbtc::DamlDecimal::parse("0.001")?,
holding_contract_ids: holding_ids,
credential_cids: Some(minter_credential_cids.clone()),
}).await?;
println!("Pending balance: {}", updated_account.pending_balance);

Submit a CBTCWithdrawAccount_Withdraw command. You can get the correct extraArgs and contract disclosures from an Attestor’s GET /cbtc/v1/token-standard-contracts endpoint. The contractIds, templateIds, and blobs in this example are for illustration only:

Terminal window
curl -X POST "${LEDGER_HOST}/v2/commands/submit-and-wait-for-transaction" \
--header 'Authorization: Bearer ${ACCESS_TOKEN}' \
--header 'Content-Type: application/json' \
--data '{
"commands": {
"commands": [
{
"ExerciseCommand": {
"templateId": "#cbtc:CBTC.WithdrawAccount:CBTCWithdrawAccount",
"contractId": "${WA_CID}",
"choice": "CBTCWithdrawAccount_Withdraw",
"choiceArgument": {
"amount": "${AMOUNT}",
"tokens": [
"${INPUT_HOLDING}"
],
"burnMintFactoryCid": "${ALLOCATION_FACTORY_CID}",
"extraArgs": {
"context": {
"values": {
"utility.digitalasset.com/instrument-configuration": {
"tag": "AV_ContractId",
"value": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5"
},
"utility.digitalasset.com/app-reward-configuration": {
"tag": "AV_ContractId",
"value": "00bee23612f3c82dd6091e64ffde81cb535e7b446d7ebdf6d91329502c6023d1cdca1112209569dade5e20bf97f963882f6387293034a82f716d2b0df28820a82048f5f86c"
},
"utility.digitalasset.com/featured-app-right": {
"tag": "AV_ContractId",
"value": "001c6e92181f8de9e4d15956e33f90db3085c518747de627134b6850459bf3662fca111220ee87bf1e3b05ed4d3f8a6c336290660f23409cb8d11a028e1073137eac8f78bf"
},
"utility.digitalasset.com/issuer-credentials": {
"tag": "AV_List",
"value": [
{
"tag": "AV_ContractId",
"value": "0058feffeeaa48cc5526c723c4877af4f6abe3c76131c54ba0d63c6da299f8681bca111220b7032514b6b448d75ea6d9eefe01f0a345504e897a5f2e72fabf974322b20b07"
}
]
}
}
},
"meta": {
"values": {
"splice.lfdecentralizedtrust.org/reason": "CBTC Burn"
}
}
}
}
}
}
],
"actAs": [
"${OWNER_PARTY}"
],
"commandId": "someCommandId",
"disclosedContracts": [
{
"templateId": "82798df018301852704f210b97adaabf76d3ecd37d889e1bf96b5f31a20eea34:Utility.Registry.App.V0.Service.AllocationFactory:AllocationFactory",
"contractId": "00d58a5f061f086b3c4b405b57ca08f4cefcb7f3a27a26089be6388eb62ee619a8ca1112200991d565daa04ca691901bc04238a8655e2c068039130e4c00027eac2675d9f4",
"createdEventBlob": "CgMyLjEShwYKRQDVil8GHwhrPEtAW1fKCPTO/LfzonomCJvmOI62LuYZqMoREiAJkdVl2qBMppGQG8BCOKhlXiwGgDkTDkwAAn6sJnXZ9BIXdXRpbGl0eS1yZWdpc3RyeS1hcHAtdjAajQEKQDgyNzk4ZGYwMTgzMDE4NTI3MDRmMjEwYjk3YWRhYWJmNzZkM2VjZDM3ZDg4OWUxYmY5NmI1ZjMxYTIwZWVhMzQSB1V0aWxpdHkSCFJlZ2lzdHJ5EgNBcHASAlYwEgdTZXJ2aWNlEhFBbGxvY2F0aW9uRmFjdG9yeRoRQWxsb2NhdGlvbkZhY3RvcnkioQJqngIKVgpUOlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmClYKVDpSY2J0Yy1uZXR3b3JrOjoxMjIwMmE4M2M2ZjQwODIyMTdjMTc1ZTI5YmM1M2RhNWYyNzAzYmEyNjc1Nzc4YWI5OTIxN2E1YTg4MWE5NDkyMDNmZgpsCmo6aGF1dGgwXzAwN2M2NWY4NTdmMWMzZDU5OWNiNmRmNzM3NzU6OjEyMjBkMmQ3MzJkMDQyYzI4MWNlZTgwZjQ4M2FiODBmM2NiYWE0NzgyODYwZWQ1ZjRkYzIyOGFiMDNkZWRkMmVlOGY5KlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmMmhhdXRoMF8wMDdjNjVmODU3ZjFjM2Q1OTljYjZkZjczNzc1OjoxMjIwZDJkNzMyZDA0MmMyODFjZWU4MGY0ODNhYjgwZjNjYmFhNDc4Mjg2MGVkNWY0ZGMyMjhhYjAzZGVkZDJlZThmOTlubOVh5DkGAEIqCiYKJAgBEiCdDhxHJbSFz7Snbvg8xLkPDPvaP3wl+HzTfq2LxHAGmRAe",
"synchronizerId": ""
},
{
"templateId": "3ca1343ab26b453d38c8adb70dca5f1ead8440c42b59b68f070786955cbf9ec1:Splice.Amulet:FeaturedAppRight",
"contractId": "001c6e92181f8de9e4d15956e33f90db3085c518747de627134b6850459bf3662fca111220ee87bf1e3b05ed4d3f8a6c336290660f23409cb8d11a028e1073137eac8f78bf",
"createdEventBlob": "CgMyLjESvQQKRQAcbpIYH43p5NFZVuM/kNswhcUYdH3mJxNLaFBFm/NmL8oREiDuh78eOwXtTT+KbDNikGYPI0CcuNEaAo4QcxN+rI94vxINc3BsaWNlLWFtdWxldBpkCkAzY2ExMzQzYWIyNmI0NTNkMzhjOGFkYjcwZGNhNWYxZWFkODQ0MGM0MmI1OWI2OGYwNzA3ODY5NTVjYmY5ZWMxEgZTcGxpY2USBkFtdWxldBoQRmVhdHVyZWRBcHBSaWdodCKqAWqnAQpNCks6SURTTzo6MTIyMGJlNThjMjllNjVkZTQwYmYyNzNiZTFkYzJiMjY2ZDQzYTlhMDAyZWE1YjE4OTU1YWVlZjdhYWM4ODFiYjQ3MWEKVgpUOlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmKklEU086OjEyMjBiZTU4YzI5ZTY1ZGU0MGJmMjczYmUxZGMyYjI2NmQ0M2E5YTAwMmVhNWIxODk1NWFlZWY3YWFjODgxYmI0NzFhMlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmOVDK9pNvQQYAQioKJgokCAESIOo7ddJGewmnI6M13zqEh5d8VRVxWJ73gao2y0uty955EB4=",
"synchronizerId": ""
},
{
"templateId": "ed73d5b9ab717333f3dbd122de7be3156f8bf2614a67360c3dd61fc0135133fa:Utility.Registry.V0.Configuration.Instrument:InstrumentConfiguration",
"contractId": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5",
"createdEventBlob": "CgMyLjESnwkKRQDaYfz6LZs1jGBvBAodY1+6vkJl0zkIdEoUioC+Dc3Dg8oREiDoZ4dGfve+Zl9ouBOjDeawSAlV3LUU7ymDJyBhg3Xe5RITdXRpbGl0eS1yZWdpc3RyeS12MBqNAQpAZWQ3M2Q1YjlhYjcxNzMzM2YzZGJkMTIyZGU3YmUzMTU2ZjhiZjI2MTRhNjczNjBjM2RkNjFmYzAxMzUxMzNmYRIHVXRpbGl0eRIIUmVnaXN0cnkSAlYwEg1Db25maWd1cmF0aW9uEgpJbnN0cnVtZW50GhdJbnN0cnVtZW50Q29uZmlndXJhdGlvbiK9BWq6BQpsCmo6aGF1dGgwXzAwN2M2NWY4NTdmMWMzZDU5OWNiNmRmNzM3NzU6OjEyMjBkMmQ3MzJkMDQyYzI4MWNlZTgwZjQ4M2FiODBmM2NiYWE0NzgyODYwZWQ1ZjRkYzIyOGFiMDNkZWRkMmVlOGY5ClYKVDpSY2J0Yy1uZXR3b3JrOjoxMjIwMmE4M2M2ZjQwODIyMTdjMTc1ZTI5YmM1M2RhNWYyNzAzYmEyNjc1Nzc4YWI5OTIxN2E1YTg4MWE5NDkyMDNmZgpWClQ6UmNidGMtbmV0d29yazo6MTIyMDJhODNjNmY0MDgyMjE3YzE3NWUyOWJjNTNkYTVmMjcwM2JhMjY3NTc3OGFiOTkyMTdhNWE4ODFhOTQ5MjAzZmYKhAEKgQFqfwpWClQ6UmNidGMtbmV0d29yazo6MTIyMDJhODNjNmY0MDgyMjE3YzE3NWUyOWJjNTNkYTVmMjcwM2JhMjY3NTc3OGFiOTkyMTdhNWE4ODFhOTQ5MjAzZmYKCAoGQgRDQlRDChsKGUIXUmVnaXN0cmFySW50ZXJuYWxTY2hlbWUKigEKhwFahAEKgQFqfwpWClQ6UmNidGMtbmV0d29yazo6MTIyMDJhODNjNmY0MDgyMjE3YzE3NWUyOWJjNTNkYTVmMjcwM2JhMjY3NTc3OGFiOTkyMTdhNWE4ODFhOTQ5MjAzZmYKCAoGQgRDQlRDChsKGUIXUmVnaXN0cmFySW50ZXJuYWxTY2hlbWUKBAoCWgAKBAoCWgAKegp4UnYKdFpyCnBqbgpaClg6VmNidGMtYmVuZWZpY2lhcnk6OjEyMjBmYTg1NDNkYjZjNjZmZTNhNTViMWYxODBjOGRmYzdmODc2MjY1Yzc2Njg0ZmJjMWQzNWQ4OWUwMmM4YWFmZThlChAKDjIMMS4wMDAwMDAwMDAwKlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmMmhhdXRoMF8wMDdjNjVmODU3ZjFjM2Q1OTljYjZkZjczNzc1OjoxMjIwZDJkNzMyZDA0MmMyODFjZWU4MGY0ODNhYjgwZjNjYmFhNDc4Mjg2MGVkNWY0ZGMyMjhhYjAzZGVkZDJlZThmOTnz2t5E5kEGAEIqCiYKJAgBEiAb19AdEjIoV/7xtMY8A2/BkXnnQHGPcCNC0UEu/b5/YxAe",
"synchronizerId": ""
},
{
"templateId": "77df4e7b980c12de438d7b052141a762215fae790d81f71179c8fb534beb68f7:Utility.Credential.V0.Credential:Credential",
"contractId": "0058feffeeaa48cc5526c723c4877af4f6abe3c76131c54ba0d63c6da299f8681bca111220b7032514b6b448d75ea6d9eefe01f0a345504e897a5f2e72fabf974322b20b07",
"createdEventBlob": "CgMyLjESjQgKRQBY/v/uqkjMVSbHI8SHevT2q+PHYTHFS6DWPG2imfhoG8oREiC3AyUUtrRI116m2e7+AfCjRVBOiXpfLnL6v5dDIrILBxIVdXRpbGl0eS1jcmVkZW50aWFsLXYwGnMKQDc3ZGY0ZTdiOTgwYzEyZGU0MzhkN2IwNTIxNDFhNzYyMjE1ZmFlNzkwZDgxZjcxMTc5YzhmYjUzNGJlYjY4ZjcSB1V0aWxpdHkSCkNyZWRlbnRpYWwSAlYwEgpDcmVkZW50aWFsGgpDcmVkZW50aWFsIuwDaukDCloKWDpWaUJUQy12YWxpZGF0b3ItMTo6MTIyMGZhODU0M2RiNmM2NmZlM2E1NWIxZjE4MGM4ZGZjN2Y4NzYyNjVjNzY2ODRmYmMxZDM1ZDg5ZTAyYzhhYWZlOGUKVgpUOlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmChIKEEIOY2J0Yy1wcm92LWhhY2sKDgoMQgpXb3JrYXJvdW5kCgQKAlIACgQKAlIACoQBCoEBWn8KfWp7ClYKVEJSY2J0Yy1uZXR3b3JrOjoxMjIwMmE4M2M2ZjQwODIyMTdjMTc1ZTI5YmM1M2RhNWYyNzAzYmEyNjc1Nzc4YWI5OTIxN2E1YTg4MWE5NDkyMDNmZgoTChFCD2hhc1JlZ2lzdHJ5Um9sZQoMCgpCCFByb3ZpZGVyCnwKemp4CnYKdGJyCnAKajpoYXV0aDBfMDA3YzY1Zjg1N2YxYzNkNTk5Y2I2ZGY3Mzc3NTo6MTIyMGQyZDczMmQwNDJjMjgxY2VlODBmNDgzYWI4MGYzY2JhYTQ3ODI4NjBlZDVmNGRjMjI4YWIwM2RlZGQyZWU4ZjkSAgoAKlJjYnRjLW5ldHdvcms6OjEyMjAyYTgzYzZmNDA4MjIxN2MxNzVlMjliYzUzZGE1ZjI3MDNiYTI2NzU3NzhhYjk5MjE3YTVhODgxYTk0OTIwM2ZmKlZpQlRDLXZhbGlkYXRvci0xOjoxMjIwZmE4NTQzZGI2YzY2ZmUzYTU1YjFmMTgwYzhkZmM3Zjg3NjI2NWM3NjY4NGZiYzFkMzVkODllMDJjOGFhZmU4ZTJoYXV0aDBfMDA3YzY1Zjg1N2YxYzNkNTk5Y2I2ZGY3Mzc3NTo6MTIyMGQyZDczMmQwNDJjMjgxY2VlODBmNDgzYWI4MGYzY2JhYTQ3ODI4NjBlZDVmNGRjMjI4YWIwM2RlZGQyZWU4Zjk5iBIJ/7RABgBCKgomCiQIARIgwrTBnrlaL0STx5Hh0v7BEmbbCERCZDgwJWqqcljhQZ0QHg==",
"synchronizerId": ""
}
]
}
}'

The cbtc-lib library provides several utility modules for managing your CBTC holdings:

Module Purpose
cbtc::batch Batch operations for sending CBTC from a CSV file
cbtc::distribute Distribute CBTC across multiple parties
cbtc::consolidate Merge multiple UTXO holdings into fewer contracts
cbtc::split Split a single holding into multiple UTXOs
cbtc::active_contracts Query your current CBTC holdings

My deposit has not been minted after 90 minutes
  • Verify the BTC transaction has at least 6 confirmations on a block explorer
  • Confirm you sent to the correct deposit address (from Step 3)
  • Check that your Canton participant node is connected and syncing
  • Escalation: contact support@bitsafe.finance
Transfer offer is not appearing for the receiver
  • The receiver must be registered in the DA Registry with a valid credential
  • Confirm the receiver’s Party ID is correct
  • Check that both parties are connected to the same Canton sync domain
"Insufficient holdings" error when sending
  • CBTC uses a UTXO model. You may need to consolidate holdings first using cbtc::consolidate
  • Check your balance with cbtc::active_contracts to verify available amounts
Authentication token expired
  • Keycloak tokens have a limited lifetime. Re-authenticate using Step 1 before retrying the operation


📧 Need help? Reach out to support@bitsafe.finance