The cBTC token supports the Canton Token Standard (CIP-0056) across all environments. More information about the token standard can be found here.
- You have a participant host that allows you to communicate with the participant node. We will refer to it later as LEDGER_HOST.
- You already have an OIDC provider set up for your Canton ecosystem — this can be Keycloak, Auth0, or any other provider.
If you want to make transfers with a token that is compliant with the token standard, it is mandatory to use the factory contracts defined by that standard. You can fetch them using the following cURL command. You will need two contracts: Utility.Registry.App.V0.Service.AllocationFactory:AllocationFactory and Utility.Registry.V0.Configuration.Instrument:InstrumentConfiguration.
curl -X POST Https://devnet.dlc.link/attestor-1/app/get-token-standard-contracts -d '{"chain": "canton-devnet"}' | jqYou need the OIDC provider so that you can act on behalf of parties on the Canton Network. This means you can log in to an account that represents the sender party who already owns cBTC. I’ve added an example request to illustrate how it works. This request will return an access token, which you should use in subsequent requests.
curl -X POST "$KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=$CLIENT_ID" \
-d "username=$USERNAME" \
-d "password=$PASSWORD" | jq
# OR
ACCESS_TOKEN=$(curl -X POST "$KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=$CLIENT_ID" \
-d "username=$USERNAME" \
-d "password=$PASSWORD" | jq -r '.access_token')The next step is to retrieve the active contracts that you can use for the transfer. First, you need to get the latest ledger end (or offset — the naming may vary) by sending the following request to the participant.
curl -X GET $LEDGER_HOST/v2/state/ledger-end \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq
# OR
LEDGER_OFFSET=$(curl -X GET "$LEDGER_HOST/v2/state/ledger-end" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.offset')After this, you can retrieve the contract IDs. Make sure to filter for the Holding interface, as it returns the token-standard-compliant active contracts. You can view the amounts in either the createArgument or the interfaceViews. Also, check the instrument.id to ensure you’re only using CBTC contracts. Select enough contracts to cover the amount you want to send to the receiving party, or simply pass all the CBTC-related contracts, and the system will automatically choose the required amount.
curl -X POST $LEDGER_HOST/v2/state/active-contracts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d "$(jq -n --arg offset "$LEDGER_OFFSET" '{
filter: {
filtersByParty: {
"$SENDER_PARTY_ID": {
cumulative: [
{
identifierFilter: {
InterfaceFilter: {
value: {
interfaceId: "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
includeInterfaceView: true,
includeCreatedEventBlob: true
}
}
}
}
]
}
}
},
verbose: false,
activeAtOffset: ($offset | tonumber)
}')" | jqWe have different decentralized parties for each environment, so make sure to use the correct one for the appropriate environment.
DECENTRALIZED_PARTY_ID=cbtc-network::12202a83c6f4082217c175e29bc53da5f2703ba2675778ab99217a5a881a949203ffDECENTRALIZED_PARTY_ID=cbtc-network::12201b1741b63e2494e4214cf0bedc3d5a224da53b3bf4d76dba468f8e97eb15508fDECENTRALIZED_PARTY_ID=cbtc-network::12205af3b949a04776fc48cdcc05a060f6bda2e470632935f375d1049a8546a3b262There is a different registry URL for each environment.
REGISTRY_URL=https://api.utilities.digitalasset-dev.comREGISTRY_URL=https://api.utilities.digitalasset-staging.comREGISTRY_URL=https://api.utilities.digitalasset.comOpen the transfer.sh file to view the cURL command used for a token-standard-compliant transfer request. The response from this request will be used later. In the choiceArgument section, you’ll find an explanation of the schema:
expectedAdmin: Always set to$DECENTRALIZED_PARTY_IDfor CBTC transfers.transfer.sender: The partyID of the sender.transfer.receiver: The partyID of the receiver.transfer.amount: The amount to be transferred.transfer.instrumentId.admin: Always set to$DECENTRALIZED_PARTY_IDfor CBTC transfers.transfer.instrumentId.id: Always set toCBTCfor CBTC transfers.transfer.requestedAt: The time when the transfer request is made - typically NOW().transfer.executeBefore: The time (exclusive) until which the transfer may be executed. This must be a future time when issuing the transfer instruction. Registries should not execute the transfer instruction after this time, allowing senders to retry by creating a new transfer instruction once it expires.transfer.inputHoldingCids: A list of contract IDs from the active contracts request.transfer.meta: Leave as is.extraArgs: Leave as is.
You can send a request to the POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/transfer-factory endpoint to retrieve the missing information required for the actual transfer submission. You can use the disclosed contracts from the response to populate the request’s disclosedContracts section. Additionally, you can use the factoryId as the contractId for the transfer submission. See the example request to this endpoint below.
curl -X POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/transfer-factory \
-H "Content-Type: application/json" \
-d '{
"choiceArguments": {
"expectedAdmin": "$DECENTRALIZED_PARTY_ID",
"transfer": {
"sender": "",
"receiver": "",
"amount": 0.002,
"instrumentId": {
"admin": "$DECENTRALIZED_PARTY_ID",
"id": "CBTC"
},
"requestedAt": "2025-11-05T18:33:30Z",
"executeBefore": "2025-11-10T18:33:47Z",
"inputHoldingCids": [
"00729ad73e6af0c69af833fc7b59ca3d949f06472664be95df7ab4ec1a8565f0fcca111220919a93e464eae32d4b159085039de601185aee0cb5a324632cf430da77c3e51a"
]
},
"extraArgs": {
"context": {
"values": {
"utility.digitalasset.com/instrument-configuration": {
"tag": "AV_ContractId",
"value": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5"
},
"utility.digitalasset.com/sender-credentials": {
"tag": "AV_List",
"value": []
},
"instrument-configuration": {
"tag": "AV_ContractId",
"value": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5"
},
"sender-credentials": {
"tag": "AV_List",
"value": []
}
}
},
"meta": {
"values": {}
}
}
},
"excludeDebugFields": true
}'The Send Transfer section returns the contract changes that occurred in the transaction. You need to locate the contract ID of the Utility.Registry.App.V0.Model.Transfer:TransferOffer template. This contract ID is required to first retrieve the context and then accept the transfer.
Before you can accept the transfer, you must obtain the context by calling the following API endpoint:
curl -X POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/$TRANSFER_OFFER_CONTRACT_ID/choice-contexts/accept \
-H "Content-Type: application/json" \
-d '{"meta":{}}'You will receive a response consisting of two main sections: choiceContextData.value and disclosedContracts. You will need both of these to accept the TransferOffer.
As the final step, you can accept the transfer. It’s important to note that this action must be submitted on behalf of the receiver party (The ACCESS_TOKEN must be able to actAs the receiver party.). This means you can only accept a transfer if you are authorized to actAs that party.
In the future, this requirement will change when Canton introduces pre-approval for the token standard.
curl -X POST $LEDGER_HOST/v2/commands/submit-and-wait-for-transaction-tree \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"commands": [
{
"ExerciseCommand": {
"templateId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
"contractId": "$TRANSFER_OFFER_CONTRACT_ID",
"choice": "TransferInstruction_Accept",
"choiceArgument": {
"extraArgs": {
"context": {
"values": $choiceContextData.value
},
"meta": {
"values": {}
}
}
}
}
}
],
"commandId": "$UUID",
"actAs": [
"$RECEIVER_PARTY"
],
"disclosedContracts": $disclosedContracts
}'