Documentation
Developer Guide
Build Canton Network applications on MainNet without running your own validator. Your parties are hosted on the VNCapital validator; you hold the keys, and nothing can be done in a party's name without a signature made with its private key.
| Base URL | https://canton.vncapital.io |
| Network | Canton Network MainNet (Global Synchronizer) |
| Software | Canton 3.5.18, Splice 0.8.3 |
| API style | JSON over HTTPS. HTTP/1.1 and HTTP/2 |
| Guide version | 2026-10-05 |
Everything in this guide was run against the live service as written. Where something was not tried by us, it says so.
1. What you get#
From VNCapital, when your project is set up:
| Item | Example | Secret? |
|---|---|---|
| Client ID | project-acme | no |
| Client secret | a long random string | yes — keep it on your server |
| Ledger user ID | 11111111-2222-3333-4444-555555555555 | no |
| Party ID(s) | alice::1220… (name, two colons, fingerprint of your key) | no |
What stays with you and never leaves your machine:
- The private key of each party (Ed25519). The party ID ends in the fingerprint of the matching public key. VNCapital never sees the private key and cannot act as your party.
What your project may do:
- Read the contracts and history of its own parties.
- Submit transactions that are signed with the party's key (create contracts, exercise choices, move tokens).
- Read public network data and the Canton Coin token registry.
What it cannot do: see other customers' data, act for any other party, manage users or parties, or submit anything unsigned.
Getting access. Projects and parties are created by VNCapital. You generate the key pair yourself and send only the public key and the party name you want; you sign one registration hash; you receive the party ID. Contact VNCapital to get started.
Generating a key (any Ed25519 tool works):
openssl genpkey -algorithm ed25519 -out party_key.pem # PRIVATE key: back it up, never share it
openssl pkey -in party_key.pem -pubout -out party_pub.pem # public key: this is what you sendThe fingerprint in your party ID is 1220 followed by the SHA-256 of the four bytes 00 00 00 0C and the raw
32-byte public key:
echo 1220$( (printf '\x00\x00\x00\x0c'; openssl pkey -in party_key.pem -pubout -outform DER | tail -c 32) | sha256sum | cut -d' ' -f1)A registered party is permanent on the network. If you lose its private key, the party can never act again. There is no recovery.
2. Five-minute start#
You need curl, jq and openssl.
BASE=https://canton.vncapital.io
CLIENT_ID=project-acme # yours
CLIENT_SECRET='…' # yours
USER_ID=11111111-2222-3333-4444-555555555555 # yours
PARTY='alice::1220…' # yours
# 1. get a token (valid 5 minutes)
TOKEN=$(curl -s -X POST $BASE/realms/customers/protocol/openid-connect/token \
-d grant_type=client_credentials -d client_id=$CLIENT_ID \
--data-urlencode "client_secret=$CLIENT_SECRET" | jq -r .access_token)
# 2. who am I, what may I do
curl -s -H "Authorization: Bearer $TOKEN" $BASE/v2/authenticated-user | jq
curl -s -H "Authorization: Bearer $TOKEN" $BASE/v2/users/$USER_ID/rights | jq
# 3. my party's contracts
END=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/v2/state/ledger-end | jq .offset)
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $BASE/v2/state/active-contracts \
-d "$(jq -n --arg p "$PARTY" --argjson end "$END" '{activeAtOffset:$end, verbose:false,
filter:{filtersByParty:{($p):{cumulative:[{identifierFilter:{WildcardFilter:{value:{includeCreatedEventBlob:false}}}}]}}}}')" | jqSubmitting a transaction is three more calls; see section 6.
3. Basics#
Address and transport#
- One host:
https://canton.vncapital.io. TLS only. - Requests and answers are JSON (
Content-Type: application/json), except the token request (form-encoded) and package downloads (binary). - Largest request body: 2 MB.
- No WebSocket streams. To follow events, poll (5.5).
Three groups of routes#
| Prefix | What | Token needed |
|---|---|---|
/realms/customers/… | Get a token | client ID + secret |
/v2/… | Canton Ledger API (JSON): your contracts, your transactions | yes |
/api/scan/…, /registry/… | Public network data and the Canton Coin token registry | yes |
Anything else answers 404 Not found.
Identifiers you will see#
| Thing | Looks like | Notes |
|---|---|---|
| Party ID | alice::1220<64 hex> | URL-encode it in paths: :: becomes %3A%3A |
| Synchronizer ID | global-domain::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc | the Global Synchronizer; read it from /v2/state/connected-synchronizers |
| DSO party | DSO::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc | the network's own party; read it from /api/scan/v0/dso-party-id |
| Contract ID | long hex string starting with 00 | |
| Update ID | hex string starting with 1220 | one committed transaction |
| Offset | integer, e.g. 5554084 | position in this validator's ledger; only grows |
| Template ID | #package-name:Module.Name:Template | write it with the package name and a leading #; answers show the package's hash instead |
Amounts and times#
- Token amounts are strings with up to 10 decimals:
"20.0000000000". - Times are UTC, ISO-8601, microseconds:
"2026-10-05T16:36:05.898347Z".
4. Authentication#
OAuth 2.0 client-credentials flow. There are no usernames or passwords.
4.1 Get a token#
POST /realms/customers/protocol/openid-connect/token — form-encoded.
curl -s -X POST https://canton.vncapital.io/realms/customers/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id=project-acme \
--data-urlencode "client_secret=$CLIENT_SECRET"{
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "profile email daml_ledger_api"
}- The token is valid for 300 seconds. There is no refresh token: request a new one when needed.
- Send it on every other call:
Authorization: Bearer <access_token>. - A wrong ID or secret answers
401. - Typical mistake: mixing up the form fields.
grant_typeis always the literalclient_credentials.
Inside the token (a signed JWT, RS256):
| Claim | Value |
|---|---|
iss | https://canton.vncapital.io/realms/customers |
aud | https://canton.network.global |
sub | your ledger user ID |
azp | your client ID |
exp | expiry, seconds since 1970 |
4.2 Public keys and discovery (optional)#
For libraries that configure themselves. No token needed.
| Route | Returns |
|---|---|
GET /realms/customers/.well-known/openid-configuration | issuer, token_endpoint, jwks_uri, … |
GET /realms/customers/protocol/openid-connect/certs | the public keys that verify tokens (JWKS) |
Only the client-credentials grant is enabled for projects, whatever the discovery document lists.
4.3 Handling the secret#
- Keep it server-side. Never ship it in a browser or mobile app.
- If it may have leaked, ask VNCapital to rotate it. The old one stops working at once.
- The secret alone cannot move anything: every transaction also needs a signature with the party's private key. It can read your parties' contracts, so treat it as confidential.
5. Reading from the ledger#
All routes in this section need Authorization: Bearer $TOKEN. In the examples:
A="Authorization: Bearer $TOKEN"; J='Content-Type: application/json'5.1 About you and the node#
| Route | Returns |
|---|---|
GET /v2/version | node version and feature flags |
GET /v2/authenticated-user | your ledger user |
GET /v2/users/{your-user-id} | the same, by ID (other users: 403) |
GET /v2/users/{your-user-id}/rights | what your user may do |
GET /v2/state/ledger-end | newest offset |
GET /v2/state/connected-synchronizers | the synchronizer ID |
GET /v2/parties/participant-id | the hosting validator's ID |
curl -s -H "$A" $BASE/v2/authenticated-user{"user": {"id": "11111111-2222-3333-4444-555555555555", "primaryParty": "", "isDeactivated": false,
"metadata": {"resourceVersion": "0", "annotations": {}}, "identityProviderId": "customers",
"primaryPartyAuthentication": false}}curl -s -H "$A" $BASE/v2/users/$USER_ID/rights{"rights": [
{"kind": {"CanExecuteAs": {"value": {"party": "alice::1220…"}}}},
{"kind": {"CanReadAs": {"value": {"party": "alice::1220…"}}}}
]}CanReadAs = read the party's contracts. CanExecuteAs = submit transactions the party has signed.
curl -s -H "$A" $BASE/v2/state/ledger-end # {"offset": 5554084}
curl -s -H "$A" $BASE/v2/state/connected-synchronizers{"connectedSynchronizers": [{"synchronizerAlias": "global",
"synchronizerId": "global-domain::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc",
"permission": "PARTICIPANT_PERMISSION_UNSPECIFIED"}]}curl -s -H "$A" $BASE/v2/parties/participant-id{"participantId": "VNCapital-validator-1::122082ce1b4e75dd486b1e8749b9fb7f09a9d40af5c4686424b41da8a41d1a426c0c"}This ID is also the validator operator's party, which you need for direct coin receiving (7.5).
5.2 Active contracts#
POST /v2/state/active-contracts — the contracts that exist for your party at a given offset.
END=$(curl -s -H "$A" $BASE/v2/state/ledger-end | jq .offset)
curl -s -H "$A" -H "$J" -X POST $BASE/v2/state/active-contracts -d "$(jq -n --arg p "$PARTY" --argjson end "$END" '{
activeAtOffset: $end,
verbose: true,
filter: {filtersByParty: {($p): {cumulative: [
{identifierFilter: {WildcardFilter: {value: {includeCreatedEventBlob: false}}}}]}}}}')"The answer is an array, one element per contract:
[{
"workflowId": "",
"contractEntry": {"JsActiveContract": {
"createdEvent": {
"offset": 5553918, "nodeId": 5,
"contractId": "00c13541edd4…",
"templateId": "fb10433a48c2…:Splice.AmuletRules:TransferPreapproval",
"createArgument": {"dso": "DSO::1220…", "receiver": "alice::1220…", "provider": "VNCapital-validator-1::1220…",
"validFrom": "2026-10-05T16:36:05.898347Z", "expiresAt": "2027-01-03T16:36:05.871903Z"},
"createdEventBlob": "", "interfaceViews": [],
"witnessParties": ["alice::1220…"],
"signatories": ["DSO::1220…", "VNCapital-validator-1::1220…", "alice::1220…"],
"observers": [],
"createdAt": "2026-10-05T16:36:05.898347Z",
"packageName": "splice-amulet"
},
"synchronizerId": "global-domain::1220…", "reassignmentCounter": 0
}}
}]Filters. Put one or more of these in cumulative:
| Filter | Selects | Body |
|---|---|---|
| Wildcard | everything the party can see | {"identifierFilter":{"WildcardFilter":{"value":{"includeCreatedEventBlob":false}}}} |
| Template | one template | {"identifierFilter":{"TemplateFilter":{"value":{"templateId":"#splice-amulet:Splice.AmuletRules:TransferPreapproval","includeCreatedEventBlob":false}}}} |
| Interface | every contract implementing an interface, with the interface's view | {"identifierFilter":{"InterfaceFilter":{"value":{"interfaceId":"#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding","includeInterfaceView":true,"includeCreatedEventBlob":false}}}} |
verbose: trueadds field names tocreateArgument.?limit=Non the URL caps the number of results.- You can only filter by parties your user may read. Another party answers
403.
5.3 One transaction#
POST /v2/updates/update-by-id (by update ID) and POST /v2/updates/update-by-offset (by offset).
curl -s -H "$A" -H "$J" -X POST $BASE/v2/updates/update-by-id -d "$(jq -n --arg p "$PARTY" --arg id "$UPDATE_ID" '{
updateId: $id,
updateFormat: {includeTransactions: {
transactionShape: "TRANSACTION_SHAPE_LEDGER_EFFECTS",
eventFormat: {verbose: true, filtersByParty: {($p): {cumulative: [
{identifierFilter: {WildcardFilter: {value: {includeCreatedEventBlob: false}}}}]}}}}}}')"{"update": {"Transaction": {"value": {
"updateId": "12201d11b890…", "commandId": "my-command-1", "workflowId": "",
"effectiveAt": "2026-10-05T16:36:03.516632Z",
"offset": 5553915,
"synchronizerId": "global-domain::1220…",
"recordTime": "2026-10-05T16:36:03.516632Z",
"paidTrafficCost": 3069,
"events": [{"CreatedEvent": {"offset": 5553915, "nodeId": 0, "contractId": "00b09e4be50f…",
"templateId": "727c0fb9711d…:Splice.Wallet.TransferPreapproval:TransferPreapprovalProposal",
"createArgument": {"receiver": "alice::1220…", "provider": "VNCapital-validator-1::1220…", "expectedDso": "DSO::1220…"},
"signatories": ["alice::1220…"], "observers": ["VNCapital-validator-1::1220…"],
"createdAt": "2026-10-05T16:36:03.516632Z", "packageName": "splice-wallet"}}]
}}}}transactionShape | You get |
|---|---|
TRANSACTION_SHAPE_ACS_DELTA | contracts created and archived (enough to keep a local copy of your contracts) |
TRANSACTION_SHAPE_LEDGER_EFFECTS | additionally the exercised choices with their arguments and results |
paidTrafficCost is the network traffic, in bytes, that the transaction cost.
For update-by-offset replace updateId with "offset": 5553915.
5.4 The life of one contract#
POST /v2/events/events-by-contract-id
curl -s -H "$A" -H "$J" -X POST $BASE/v2/events/events-by-contract-id -d "$(jq -n --arg p "$PARTY" --arg c "$CONTRACT_ID" '{
contractId: $c,
eventFormat: {verbose: false, filtersByParty: {($p): {cumulative: [
{identifierFilter: {WildcardFilter: {value: {includeCreatedEventBlob: false}}}}]}}}}')"Answer: {"created": {"createdEvent": {…}, "synchronizerId": "…"}, "archived": null}. archived is null while
the contract exists, and holds the archiving event afterwards.
5.5 Following new events (polling)#
POST /v2/updates/get-updates-page — a page of everything that happened to your party after an offset.
curl -s -H "$A" -H "$J" -X POST $BASE/v2/updates/get-updates-page -d "$(jq -n --arg p "$PARTY" --argjson from "$LAST_SEEN_OFFSET" '{
beginOffsetExclusive: $from,
maxPageSize: 100,
updateFormat: {includeTransactions: {
transactionShape: "TRANSACTION_SHAPE_ACS_DELTA",
eventFormat: {verbose: false, filtersByParty: {($p): {cumulative: [
{identifierFilter: {WildcardFilter: {value: {includeCreatedEventBlob: false}}}}]}}}}}}')"{"updates": [{"update": {"Transaction": {"value": {"updateId": "1220…", "offset": 5553915, "events": [ … ]}}}}],
"lowestPageOffsetExclusive": 5553914,
"highestPageOffsetInclusive": 5553918,
"nextPageToken": "…"}A simple follow loop (verified):
- Remember the offset you have processed up to. Start with the current
ledger-end, or0for the full history. - Call with
beginOffsetExclusive= that offset. - Process
updates(it may be empty). Set your offset tohighestPageOffsetInclusive: the page covers everything up to and including it, so nothing is skipped even whenmaxPageSizecut the page short. - If
updateswas empty, wait a second or two. Go to 2.
Alternatively keep the first request's body unchanged and add "pageToken": "<nextPageToken>" from the previous
answer on every following call; with no end offset the token is always present, and this "tails" the ledger.
Older routes that still work but are marked deprecated by Canton: /v2/updates/flats, /v2/updates/trees,
/v2/updates/transaction-by-id, /v2/updates/transaction-by-offset. Prefer the ones above.
5.6 Packages#
The Daml packages installed on the validator. Your application's own package must be installed before you can use its templates: send the DAR file to VNCapital.
| Route | Returns |
|---|---|
GET /v2/packages | {"packageIds": ["…", …]} |
GET /v2/packages/{package-id}/status | {"packageStatus": "PACKAGE_STATUS_REGISTERED"} |
GET /v2/packages/{package-id} | the package file (binary, application/octet-stream) |
GET /v2/interactive-submission/preferred-package-version?package-name=…&parties=… | which version of a package name the node will use for a party |
POST /v2/interactive-submission/preferred-packages | the same for several package names at once |
curl -s -H "$A" "$BASE/v2/interactive-submission/preferred-package-version?package-name=splice-amulet&parties=$(jq -rn --arg p "$PARTY" '$p|@uri')"{"packagePreference": {"packageReference": {"packageId": "fb10433a48c2…", "packageName": "splice-amulet", "packageVersion": "0.1.22"},
"synchronizerId": "global-domain::1220…"}}curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/preferred-packages -d "$(jq -n --arg p "$PARTY" --arg s "$SYNC" '{
packageVettingRequirements: [{packageName: "splice-amulet", parties: [$p]}], synchronizerId: $s}')"5.7 The full Ledger API description#
GET /docs/openapi returns the node's complete OpenAPI document (YAML). It lists more routes than are
reachable here; only the routes in this guide are open.
6. Submitting transactions#
A transaction for a self-custody party is made in three steps. The validator builds it, you sign it, the validator sends it.
1. prepare POST /v2/interactive-submission/prepare → transaction + hash (writes nothing)
2. sign on your machine, with the party's private key → signature
3. execute POST /v2/interactive-submission/executeAndWait → committed on MainNetRules:
- One command per transaction. A transaction signed by an external key may have only one root action. Create and exercise in the same request is refused ("multiple root nodes").
- Every committed transaction is permanent and uses network traffic (typically 3 KB for a small contract, 5-8 KB for a coin transfer).
- A wrong signature is refused before anything is sent to the network.
6.1 Prepare#
POST /v2/interactive-submission/prepare
SYNC='global-domain::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc'
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/prepare -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg s "$SYNC" '{
userId: $u,
commandId: "my-command-1",
actAs: [$p],
readAs: [],
synchronizerId: $s,
packageIdSelectionPreference: [],
verboseHashing: false,
commands: [ {CreateCommand: {
templateId: "#your-package:Your.Module:YourTemplate",
createArguments: {owner: $p, text: "hello"} }} ]
}')" > prepared.json| Field | Meaning |
|---|---|
userId | your ledger user ID |
commandId | your own unique string for this command; reuse it only to retry the very same command |
actAs | the party that signs. One of your parties |
readAs | other parties of yours whose contracts the command needs to read; usually [] |
synchronizerId | the Global Synchronizer ID (required) |
packageIdSelectionPreference | usually [] (required) |
verboseHashing | true adds hashingDetails: a readable listing of exactly what is hashed |
commands | exactly one command, see below |
disclosedContracts | contracts the command needs that your party cannot see itself (used for coin, section 7) |
Commands:
{"CreateCommand": {"templateId": "#pkg:Mod:Tpl", "createArguments": { … }}}
{"ExerciseCommand": {"templateId": "#pkg:Mod:Tpl", "contractId": "00…", "choice": "ChoiceName", "choiceArgument": { … }}}For a choice defined on an interface, put the interface ID in templateId.
Every contract has the built-in choice Archive with "choiceArgument": {} (the signatories may use it).
Answer:
{
"preparedTransaction": "CtUGCgMyLjESATAapgYK…",
"preparedTransactionHash": "j/PkXEFOMREFdltLs5y3zGuCUsAoTEjnzt7+oyXCNmU=",
"hashingSchemeVersion": "HASHING_SCHEME_VERSION_V2",
"hashingDetails": null,
"costEstimation": {"estimationTimestamp": "2026-10-05T16:59:09.902976Z",
"confirmationRequestTrafficCostEstimation": 3103,
"confirmationResponseTrafficCostEstimation": 0,
"totalTrafficCostEstimation": 3103}
}preparedTransaction: the full transaction (base64). Send it back unchanged in step 3.preparedTransactionHash: 32 bytes (base64). This is what you sign.costEstimation.totalTrafficCostEstimation: expected traffic in bytes. In our measurements the real cost was never above it and up to a third below.
Prepare changes nothing on the ledger. Execute soon after preparing; if the node refuses an old prepared transaction, prepare again.
Know what you sign. The hash commits to the whole transaction. A careful client decodes
preparedTransactionand recomputes the hash itself instead of trusting the node; the Canton Wallet SDK does this.verboseHashing: trueshows what goes into the hash.
6.2 Sign#
Plain Ed25519 over the raw 32 bytes of the hash (decode the base64 first; do not hash again).
With OpenSSL:
jq -r .preparedTransactionHash prepared.json | base64 -d > hash.bin
SIG=$(openssl pkeyutl -sign -inkey party_key.pem -rawin -in hash.bin | base64 -w0)With Python (pip install cryptography):
import base64
from cryptography.hazmat.primitives import serialization
key = serialization.load_pem_private_key(open("party_key.pem", "rb").read(), password=None)
signature_b64 = base64.b64encode(key.sign(base64.b64decode(prepared["preparedTransactionHash"]))).decode()With Node.js (standard crypto module; not run by us):
const crypto = require("crypto"), fs = require("fs");
const key = crypto.createPrivateKey(fs.readFileSync("party_key.pem"));
const signatureB64 = crypto.sign(null, Buffer.from(prepared.preparedTransactionHash, "base64"), key).toString("base64");6.3 Execute#
POST /v2/interactive-submission/executeAndWait — sends the transaction and waits until it is committed.
FINGERPRINT=${PARTY##*::} # the part of the party ID after the two colons
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/executeAndWait -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg fp "$FINGERPRINT" --arg sig "$SIG" \
--arg tx "$(jq -r .preparedTransaction prepared.json)" \
--arg hv "$(jq -r .hashingSchemeVersion prepared.json)" '{
preparedTransaction: $tx,
hashingSchemeVersion: $hv,
submissionId: "my-submission-1",
userId: $u,
deduplicationPeriod: {Empty: {}},
partySignatures: {signatures: [{party: $p, signatures: [{
format: "SIGNATURE_FORMAT_CONCAT",
signature: $sig,
signedBy: $fp,
signingAlgorithmSpec: "SIGNING_ALGORITHM_SPEC_ED25519"}]}]}
}')"{"updateId": "12201d11b890b3e4eeeca9c55bbc0043e7e71ffbc1b30ac8ed097e18e4a8f232bd48", "completionOffset": 5553915}The transaction is now on MainNet. Read it back with 5.3.
| Variant | Behaviour |
|---|---|
/v2/interactive-submission/executeAndWait | waits; returns updateId and completionOffset. Used throughout this guide |
/v2/interactive-submission/executeAndWaitForTransaction | waits; returns the whole transaction. Takes an optional transactionFormat (same shape as in 5.3). Not run by us |
/v2/interactive-submission/execute | returns at once without waiting; find the result later by polling. Not run by us |
A wrong signature:
{"code": "FAILED_TO_EXECUTE_TRANSACTION",
"cause": "The participant failed to execute the transaction: Received 0 valid signatures from distinct keys (1 invalid), but expected at least 1 valid for alice::1220…. Transaction hash to be signed: 1220de87…. Ensure the correct transaction hash is signed with the correct key(s)."}HTTP 400; nothing was sent to the network and no traffic was used.
6.4 Worked example: exercise a choice#
Archive a contract your party created:
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/prepare -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg s "$SYNC" --arg c "$CONTRACT_ID" '{
userId: $u, commandId: "archive-1", actAs: [$p], readAs: [], synchronizerId: $s,
packageIdSelectionPreference: [], verboseHashing: false,
commands: [ {ExerciseCommand: {
templateId: "#your-package:Your.Module:YourTemplate",
contractId: $c, choice: "Archive", choiceArgument: {} }} ]
}')" > prepared.json
# then sign (6.2) and execute (6.3)7. Canton Coin and other tokens#
Canton Coin (CC, instrument ID Amulet) and other CIP-56 tokens are handled through the token standard:
you read holdings from the ledger, ask the token's registry for the details of a transfer, and submit with
the same prepare / sign / execute as any other transaction.
For Canton Coin the registry is available on this service under /registry/… (section 8).
Another token has its own registry address, published by its issuer; the ledger calls are the same.
Interfaces used (token standard v1):
| Purpose | Interface ID |
|---|---|
| Holdings | #splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding |
| Pending transfers | #splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction |
| Starting a transfer | #splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory |
Measured with real coin on MainNet: accepting a transfer used 5.6-7.2 KB of traffic, sending 5.1-6.1 KB. The full amount arrived; no fee was taken from it.
7.1 Your balance: holdings#
curl -s -H "$A" -H "$J" -X POST $BASE/v2/state/active-contracts -d "$(jq -n --arg p "$PARTY" --argjson end "$END" '{
activeAtOffset: $end, verbose: false,
filter: {filtersByParty: {($p): {cumulative: [{identifierFilter: {InterfaceFilter: {value: {
interfaceId: "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
includeInterfaceView: true, includeCreatedEventBlob: false}}}}]}}}}')"Each result carries the interface's view in createdEvent.interfaceViews[0].viewValue:
{"owner": "alice::1220…",
"instrumentId": {"admin": "DSO::1220…", "id": "Amulet"},
"amount": "20.0000000000",
"lock": null,
"meta": {"values": {}}}Your balance of a token is the sum of amount over its holdings. A holding with a lock is reserved (for
example by a transfer you started that is not accepted yet).
7.2 Incoming transfers waiting for you#
The same query with the TransferInstruction interface. View of one pending transfer:
{"originalInstructionCid": null,
"transfer": {"sender": "bob::1220…", "receiver": "alice::1220…", "amount": "20.0000000000",
"instrumentId": {"admin": "DSO::1220…", "id": "Amulet"},
"requestedAt": "2026-10-05T01:11:58.094Z", "executeBefore": "2026-10-06T01:11:58.094Z",
"inputHoldingCids": ["00…"], "meta": {"values": {}}},
"status": {"tag": "TransferPendingReceiverAcceptance", "value": {}},
"meta": {"values": {}}}An offer expires at executeBefore (24 hours when sent from the standard wallet).
7.3 Accept an incoming transfer#
- Ask the registry for the context of the acceptance:
CID='00e6a9d183ef…' # contractId of the pending transfer from 7.2
curl -s -H "$A" -H "$J" -X POST "$BASE/registry/transfer-instruction/v1/$CID/choice-contexts/accept" \
-d '{"meta": {}, "excludeDebugFields": true}' > ctx.json{"choiceContextData": {"values": {"amulet-rules": {…}, "open-round": {…}, "external-party-config-state": {…}, "expire-lock": {…}}},
"disclosedContracts": [{"templateId": "…", "contractId": "00…", "createdEventBlob": "…", "synchronizerId": "global-domain::1220…"}, …]}- Prepare the acceptance, passing the context and the disclosed contracts through unchanged:
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/prepare -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg s "$SYNC" --arg c "$CID" --slurpfile ctx ctx.json '{
userId: $u, commandId: "accept-1", actAs: [$p], readAs: [], synchronizerId: $s,
packageIdSelectionPreference: [], verboseHashing: false,
commands: [ {ExerciseCommand: {
templateId: "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
contractId: $c,
choice: "TransferInstruction_Accept",
choiceArgument: {extraArgs: {context: $ctx[0].choiceContextData, meta: {values: {}}}} }} ],
disclosedContracts: [ $ctx[0].disclosedContracts[] | {templateId, contractId, createdEventBlob, synchronizerId} ]
}')" > prepared.json- Sign and execute as in 6.2 and 6.3. Afterwards the coin is a holding of your party (7.1).
The prepared transaction is larger than usual, about 27 KB, because of the disclosed contracts.
To decline instead: …/choice-contexts/reject and the choice TransferInstruction_Reject (not run by us).
7.4 Send#
- Collect the contract IDs of your unlocked holdings of the token (7.1).
- Ask the registry for the transfer factory:
DSO=$(curl -s -H "$A" $BASE/api/scan/v0/dso-party-id | jq -r .dso_party_id)
# (GNU date, as on Linux)
NOW=$(date -u -d '-30 seconds' +%Y-%m-%dT%H:%M:%S.000000Z) # must be in the past
BEFORE=$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%S.000000Z) # latest time the transfer may complete
jq -n --arg dso "$DSO" --arg from "$PARTY" --arg to "$RECEIVER" --arg amt "20.0000000000" \
--arg now "$NOW" --arg before "$BEFORE" --argjson holdings '["00…holding-1…"]' '{
expectedAdmin: $dso,
transfer: {sender: $from, receiver: $to, amount: $amt,
instrumentId: {admin: $dso, id: "Amulet"},
requestedAt: $now, executeBefore: $before,
inputHoldingCids: $holdings, meta: {values: {}}},
extraArgs: {context: {values: {}}, meta: {values: {}}}
}' > transfer.json
curl -s -H "$A" -H "$J" -X POST $BASE/registry/transfer-instruction/v1/transfer-factory \
-d "$(jq -n --slurpfile t transfer.json '{choiceArguments: $t[0], excludeDebugFields: true}')" > factory.json{"factoryId": "00…",
"transferKind": "direct",
"choiceContext": {"choiceContextData": {"values": { … }}, "disclosedContracts": [ … ]}}transferKind | What will happen |
|---|---|
direct | the receiver has a transfer preapproval: the coin arrives in this one transaction |
offer | the receiver must accept (7.3) before executeBefore; until then your holdings are locked |
self | sender and receiver are the same party (merging or splitting holdings) |
- Prepare the transfer: the same arguments, with the registry's context filled in.
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/prepare -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg s "$SYNC" --slurpfile t transfer.json --slurpfile f factory.json '{
userId: $u, commandId: "send-1", actAs: [$p], readAs: [], synchronizerId: $s,
packageIdSelectionPreference: [], verboseHashing: false,
commands: [ {ExerciseCommand: {
templateId: "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory",
contractId: $f[0].factoryId,
choice: "TransferFactory_Transfer",
choiceArgument: ($t[0] | .extraArgs.context = $f[0].choiceContext.choiceContextData) }} ],
disclosedContracts: [ $f[0].choiceContext.disclosedContracts[] | {templateId, contractId, createdEventBlob, synchronizerId} ]
}')" > prepared.json- Sign and execute (6.2, 6.3).
Notes:
amountmay be less than the sum of the input holdings; the rest comes back to you as a new holding.- To take back an offer that was not accepted:
…/choice-contexts/withdrawandTransferInstruction_Withdraw(not run by us).
7.5 Receive directly: transfer preapproval#
Without a preapproval every incoming Canton Coin transfer is an offer you must accept. With one, coin arrives
in a single step and senders see transferKind: direct.
Your party asks the validator to provide the preapproval by creating one contract:
OPERATOR=$(curl -s -H "$A" $BASE/v2/parties/participant-id | jq -r .participantId)
DSO=$(curl -s -H "$A" $BASE/api/scan/v0/dso-party-id | jq -r .dso_party_id)
curl -s -H "$A" -H "$J" -X POST $BASE/v2/interactive-submission/prepare -d "$(jq -n \
--arg u "$USER_ID" --arg p "$PARTY" --arg s "$SYNC" --arg op "$OPERATOR" --arg dso "$DSO" '{
userId: $u, commandId: "preapproval-1", actAs: [$p], readAs: [], synchronizerId: $s,
packageIdSelectionPreference: [], verboseHashing: false,
commands: [ {CreateCommand: {
templateId: "#splice-wallet:Splice.Wallet.TransferPreapproval:TransferPreapprovalProposal",
createArguments: {receiver: $p, provider: $op, expectedDso: $dso} }} ]
}')" > prepared.json
# then sign (6.2) and execute (6.3)The validator accepts the request automatically, in our test 2.4 seconds later. Check:
curl -s -H "$A" "$BASE/api/scan/v0/transfer-preapprovals/by-party/$(jq -rn --arg p "$PARTY" '$p|@uri')"{"transfer_preapproval": {"contract": {
"template_id": "…:Splice.AmuletRules:TransferPreapproval", "contract_id": "00c13541edd4…",
"payload": {"dso": "DSO::1220…", "receiver": "alice::1220…", "provider": "VNCapital-validator-1::1220…",
"validFrom": "2026-10-05T16:36:05.898347Z", "expiresAt": "2027-01-03T16:36:05.871903Z"}}}}404 means the party has no preapproval.
- It is valid for 90 days and renewed by the validator before it expires.
- The provider (here the validator operator) is recorded as the application behind incoming transfers that use the preapproval, and takes part in them.
- You can end it at any time with the choice
TransferPreapproval_Cancelon the preapproval contract (not run by us). - A preapproval covers Canton Coin. Other tokens define their own rules for receiving.
8. Network data (Scan) and the token registry#
Public Canton Network data, relayed from the Super Validators' Scan service: network rules, mining rounds, which validator hosts a party, the name service, public history, balances as of a snapshot, and the Canton Coin registry.
- Routes:
GETandPOSTunder/api/scan/v0/…,/api/scan/v2/…and/registry/…. - A project token is required. Requests count towards your project's limits.
- The answer comes from one Super Validator at a time and is passed on unchanged. If you need certainty for a
decision, compare with other Scan instances (
/api/scan/v0/scanslists them). - This is network-wide public data. It is not the place to read your own contracts: use section 5.
8.1 Network facts#
| Route | Returns |
|---|---|
GET /api/scan/v0/dso-party-id | {"dso_party_id": "DSO::1220…"} |
GET /api/scan/v0/dso | the network's governing rules and the Super Validators (large, about 70 KB) |
GET /api/scan/v0/splice-instance-names | display names: "Canton Network", "Canton Coin", "CC", "Canton Name Service" |
GET /api/scan/v0/scans | all Scan instances with their public addresses |
GET /api/scan/v0/dso-sequencers | the network's sequencers |
8.2 Canton Coin rules and rounds#
| Route | Body | Returns |
|---|---|---|
POST /api/scan/v0/amulet-rules | {} | the current coin rules contract (amulet_rules_update.contract) |
POST /api/scan/v0/open-and-issuing-mining-rounds | {"cached_open_mining_round_contract_ids": [], "cached_issuing_round_contract_ids": []} | open and issuing rounds, each with the coin price (amuletPrice, USD per CC) |
GET /api/scan/v0/closed-rounds | recently closed rounds | |
POST /api/scan/v0/external-party-amulet-rules | {} | rules contract used by externally signed coin commands |
POST /api/scan/v0/ans-rules | {} | name-service rules |
8.3 About a party or a validator#
| Route | Returns |
|---|---|
GET /api/scan/v0/domains/{synchronizer-id}/parties/{party}/participant-id | which validator hosts the party: {"participant_id": "PAR::…"} |
GET /api/scan/v0/transfer-preapprovals/by-party/{party} | the party's transfer preapproval, or 404 |
GET /api/scan/v0/featured-apps/{party} | {"featured_app_right": null} or the right |
GET /api/scan/v0/featured-apps | all featured applications (large) |
GET /api/scan/v0/transfer-command-counter/{party} | counter for the legacy transfer command; 404 with "use 0 for the nonce" if none |
GET /api/scan/v0/domains/{synchronizer-id}/members/{member-id}/traffic-status | a validator's traffic: consumed, limit, purchased. member-id is PAR:: + participant ID |
GET /api/scan/v0/validators/validator-faucets?validator_ids={validator-party} | a validator's liveness record |
GET /api/scan/v0/admin/validator/licenses?limit=N | validator licences on the network |
Party and synchronizer IDs in paths must be URL-encoded:
P=$(jq -rn --arg p "$PARTY" '$p|@uri'); S=$(jq -rn --arg s "$SYNC" '$s|@uri')
curl -s -H "$A" "$BASE/api/scan/v0/domains/$S/parties/$P/participant-id"8.4 Name service#
| Route | Returns |
|---|---|
GET /api/scan/v0/ans-entries?page_size=N | name entries; optional name_prefix= |
GET /api/scan/v0/ans-entries/by-name/{name} | one entry, or 404 |
GET /api/scan/v0/ans-entries/by-party/{party} | the first entry of a party, or 404 |
8.5 Public history and snapshots#
| Route | Body / query | Returns |
|---|---|---|
POST /api/scan/v2/updates | {"page_size": 10}; continue with "after": {"after_migration_id": M, "after_record_time": "…"} taken from the last item | the network's public transaction history, oldest first |
GET /api/scan/v2/updates/{update-id} | one public update | |
GET /api/scan/v0/state/acs/snapshot-timestamp?before={time}&migration_id=4 | the time of the latest snapshot before time: {"record_time": "2026-10-05T15:00:00Z"} | |
POST /api/scan/v0/holdings/summary | {"migration_id": 4, "record_time": "<snapshot time>", "owner_party_ids": ["…"]} | coin totals per party as of the snapshot |
POST /api/scan/v0/holdings/state | {"migration_id": 4, "record_time": "<snapshot time>", "page_size": 100, "owner_party_ids": ["…"]} | the coin contracts behind those totals |
POST /api/scan/v0/state/acs | {"migration_id": 4, "record_time": "<snapshot time>", "page_size": 100, "templates": ["package-name:Module:Entity"]} | contracts of public templates as of the snapshot |
migration_id is the network's current migration number, 4 at the time of writing (it appears in
/api/scan/v0/dso-sequencers). Snapshots are taken every few hours, so these balances lag behind; for your own
party's live balance use 7.1.
SNAP=$(curl -s -H "$A" "$BASE/api/scan/v0/state/acs/snapshot-timestamp?before=$(date -u +%Y-%m-%dT%H:%M:%SZ)&migration_id=4" | jq -r .record_time)
curl -s -H "$A" -H "$J" -X POST $BASE/api/scan/v0/holdings/summary \
-d "$(jq -n --arg t "$SNAP" --arg p "$PARTY" '{migration_id: 4, record_time: $t, owner_party_ids: [$p]}')"{"record_time": "2026-10-05T15:00:00Z", "migration_id": 4, "computed_as_of_round": 116116,
"summaries": [{"party_id": "alice::1220…", "total_unlocked_coin": "125.0000000000", "total_locked_coin": "0.0000000000",
"total_coin_holdings": "125.0000000000", "accumulated_holding_fees_total": "0.0100000000", "…": "…"}]}8.6 Token registry for Canton Coin#
| Route | Returns |
|---|---|
GET /registry/metadata/v1/info | {"adminId": "DSO::1220…", "supportedApis": {…}} |
GET /registry/metadata/v1/instruments | the instruments of this registry |
GET /registry/metadata/v1/instruments/Amulet | Canton Coin: name, symbol CC, 10 decimals, total supply, supported token APIs |
POST /registry/transfer-instruction/v1/transfer-factory | start a transfer (7.4) |
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/accept | context to accept (7.3) |
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/reject | context to reject |
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/withdraw | context to withdraw your own offer |
The registry also offers the allocation APIs of the token standard (used for delivery-versus-payment) and the v2
interfaces; they are reachable under /registry/… but were not run by us.
8.7 Not relayed#
/api/scan/v0/state/acs/force, /api/scan/v0/backfilling/…, /api/scan/version, /api/scan/readyz, anything
outside v0, v2 and /registry, and methods other than GET and POST answer 404.
9. Errors and limits#
9.1 HTTP status codes#
| Code | Meaning | What to do |
|---|---|---|
| 200 | success | |
| 400 | the request is wrong: missing field, bad value, invalid signature, refused command | read the message; fix the request |
| 401 | no valid token: missing on a Scan/registry route, expired, malformed, wrong signature | get a new token |
| 403 | the token is valid but may not do this: another party's data, another user, a project that is not active | check the party and your rights |
| 404 | the route is not available here, or a Ledger API route was called without a Bearer token, or the thing does not exist (Scan lookups) | check the path and the Authorization header |
| 413 | body larger than 2 MB | |
| 429 | rate limit reached. The answer has a Retry-After header in seconds | wait that long, then retry |
| 502 / 503 | temporarily unavailable | retry after a few seconds |
9.2 Error bodies#
From the service itself (token, limits, availability):
{"error": "rate limit of this project reached", "detail": null}Messages you may see: missing bearer token, malformed token, bad token signature, token expired,
not a customer project, project is not active, rate limit of this project reached.
From the Ledger API:
{"code": "INVALID_ARGUMENT", "cause": "The submitted request has invalid arguments: …",
"correlationId": "…", "traceId": "…", "context": {}, "resources": [], "errorCategory": 8, "grpcCodeValue": 3}codeandcausesay what happened. QuotetraceIdwhen asking for support.- Permission problems are deliberately vague:
"cause": "A security-sensitive error has been received"with HTTP 403 (grpcCodeValue7). It means: your user may not read or act for that party, user or contract. - A structurally wrong body is answered in plain text:
Invalid value for: body (Missing required field at 'synchronizerId', …).
From the Scan relay: {"error": "No ans entry found for name: …"}.
9.3 Limits#
| Limit | Default |
|---|---|
| Requests per project (ledger, Scan and registry together) | 120 per minute, bursts of 40 |
Submissions per project (execute*) | 20 per minute, bursts of 10 |
| Token requests per client address | 60 per minute, bursts of 20 |
| Request body | 2 MB |
| Token lifetime | 5 minutes |
| Commands per transaction | 1 |
Your plan may have other limits. Get one token and reuse it for its five minutes instead of requesting a token per call.
9.4 Retrying safely#
- Reads can always be retried.
- A submission that timed out on your side may still have been committed. Before retrying, look for it with
5.5 (the
commandIdis in the transaction), or retry with the samecommandIdandsubmissionId. - After a
429wait forRetry-After. Do not retry in a tight loop.
10. What is not available#
| Not available | Instead |
|---|---|
| Creating users or parties through the API | done by VNCapital when your project is set up |
| Installing Daml packages yourself | send the DAR file to VNCapital |
Transactions signed by the validator on your behalf (/v2/commands/…) | prepare, sign with your own key, execute (section 6) |
| WebSocket streams | poll get-updates-page (5.5) |
| Listing other users, parties or identity settings | — |
| The validator's own wallet and administration API | — |
| A test network | this service is MainNet: every transaction is real and permanent. Develop against a local Canton network first |
| Several commands in one transaction | one command per transaction; put multi-step logic into a Daml choice |
11. Complete example in Python#
Reads the party's contracts and submits one command. Needs Python 3.10+ and pip install cryptography.
#!/usr/bin/env python3
import base64, json, secrets, urllib.parse, urllib.request, urllib.error
from cryptography.hazmat.primitives import serialization
BASE = "https://canton.vncapital.io"
CLIENT_ID = "project-acme" # yours
CLIENT_SECRET = "…" # yours; load it from a safe place
USER_ID = "11111111-2222-3333-4444-555555555555" # yours
PARTY = "alice::1220…" # yours
KEY_FILE = "party_key.pem" # the party's private key
def http(method, path, token=None, body=None, form=None):
headers, data = {}, None
if token:
headers["Authorization"] = "Bearer " + token
if body is not None:
data, headers["Content-Type"] = json.dumps(body).encode(), "application/json"
if form is not None:
data, headers["Content-Type"] = urllib.parse.urlencode(form).encode(), "application/x-www-form-urlencoded"
req = urllib.request.Request(BASE + path, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=120) as r:
return r.status, json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
raw = e.read()
try:
return e.code, json.loads(raw)
except Exception:
return e.code, raw.decode(errors="replace")
def get_token():
status, r = http("POST", "/realms/customers/protocol/openid-connect/token",
form={"grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET})
assert status == 200, r
return r["access_token"] # valid 300 seconds
def wildcard(party):
return {"filtersByParty": {party: {"cumulative": [
{"identifierFilter": {"WildcardFilter": {"value": {"includeCreatedEventBlob": False}}}}]}}}
def active_contracts(token):
_, end = http("GET", "/v2/state/ledger-end", token)
status, rows = http("POST", "/v2/state/active-contracts", token,
{"activeAtOffset": end["offset"], "verbose": True, "filter": wildcard(PARTY)})
assert status == 200, rows
return [row["contractEntry"]["JsActiveContract"]["createdEvent"] for row in rows]
def submit(token, command, disclosed=()):
"""prepare -> sign -> execute. Returns (update id, offset)."""
_, sync = http("GET", "/v2/state/connected-synchronizers", token)
status, prep = http("POST", "/v2/interactive-submission/prepare", token, {
"userId": USER_ID, "commandId": "cmd-" + secrets.token_hex(8), "actAs": [PARTY], "readAs": [],
"synchronizerId": sync["connectedSynchronizers"][0]["synchronizerId"],
"packageIdSelectionPreference": [], "verboseHashing": False,
"commands": [command], "disclosedContracts": list(disclosed)})
assert status == 200, prep
print("estimated traffic:", prep["costEstimation"]["totalTrafficCostEstimation"], "bytes")
key = serialization.load_pem_private_key(open(KEY_FILE, "rb").read(), password=None)
signature = key.sign(base64.b64decode(prep["preparedTransactionHash"])) # raw 32 bytes, Ed25519
status, done = http("POST", "/v2/interactive-submission/executeAndWait", token, {
"preparedTransaction": prep["preparedTransaction"],
"hashingSchemeVersion": prep["hashingSchemeVersion"],
"submissionId": "sub-" + secrets.token_hex(8), "userId": USER_ID,
"deduplicationPeriod": {"Empty": {}},
"partySignatures": {"signatures": [{"party": PARTY, "signatures": [{
"format": "SIGNATURE_FORMAT_CONCAT", "signature": base64.b64encode(signature).decode(),
"signedBy": PARTY.split("::")[1], "signingAlgorithmSpec": "SIGNING_ALGORITHM_SPEC_ED25519"}]}]}})
assert status == 200, done
return done["updateId"], done["completionOffset"]
if __name__ == "__main__":
token = get_token()
for c in active_contracts(token):
print(c["contractId"][:16], c["templateId"].split(":", 1)[1])
# One command per transaction. Replace with a template of a package installed for you.
update_id, offset = submit(token, {"CreateCommand": {
"templateId": "#your-package:Your.Module:YourTemplate",
"createArguments": {"owner": PARTY, "text": "hello"}}})
print("committed:", update_id, "at offset", offset)12. All routes at a glance#
T = needs a project token. Everything is under https://canton.vncapital.io.
Authentication
| Method | Route | T | Purpose |
|---|---|---|---|
| POST | /realms/customers/protocol/openid-connect/token | – | get a token |
| GET | /realms/customers/protocol/openid-connect/certs | – | public keys |
| GET | /realms/customers/.well-known/openid-configuration | – | discovery |
Ledger API — read
| Method | Route | T | Purpose |
|---|---|---|---|
| GET | /v2/version | T | node version |
| GET | /v2/authenticated-user | T | your user |
| GET | /v2/users/{user-id} | T | your user by ID |
| GET | /v2/users/{user-id}/rights | T | your rights |
| GET | /v2/state/ledger-end | T | newest offset |
| GET | /v2/state/connected-synchronizers | T | synchronizer ID |
| GET | /v2/parties/participant-id | T | validator ID (= operator party) |
| POST | /v2/state/active-contracts | T | current contracts |
| POST | /v2/updates/update-by-id | T | one transaction by ID |
| POST | /v2/updates/update-by-offset | T | one transaction by offset |
| POST | /v2/updates/get-updates-page | T | history / follow new events |
| POST | /v2/updates/flats, /trees, /transaction-by-id, /transaction-by-offset | T | deprecated equivalents |
| POST | /v2/events/events-by-contract-id | T | create/archive events of one contract |
| GET | /v2/packages | T | installed packages |
| GET | /v2/packages/{package-id} | T | download a package |
| GET | /v2/packages/{package-id}/status | T | package status |
| GET | /v2/interactive-submission/preferred-package-version | T | package version for a party |
| POST | /v2/interactive-submission/preferred-packages | T | package versions for several names |
| GET | /docs/openapi | T | full Ledger API description |
Ledger API — write (signed by your key)
| Method | Route | T | Purpose |
|---|---|---|---|
| POST | /v2/interactive-submission/prepare | T | build a transaction and its hash (writes nothing) |
| POST | /v2/interactive-submission/executeAndWait | T | submit a signed transaction and wait |
| POST | /v2/interactive-submission/executeAndWaitForTransaction | T | same, returns the transaction |
| POST | /v2/interactive-submission/execute | T | submit without waiting |
| POST | /v2/parties/external/generate-topology | T | draft of a party registration (writes nothing; registration itself is completed by VNCapital) |
Network data and token registry
| Method | Route | T | Purpose |
|---|---|---|---|
| GET, POST | /api/scan/v0/…, /api/scan/v2/… | T | public network data (section 8) |
| GET, POST | /registry/… | T | Canton Coin token registry (sections 7 and 8.6) |
13. Glossary#
| Term | Meaning |
|---|---|
| Party | an identity on the ledger that owns contracts and signs transactions. name::fingerprint |
| Self-custody (external) party | a party whose signing key is held by you, not by the validator |
| Validator (participant) | the node that hosts parties, stores their contracts and submits their transactions |
| Synchronizer | the shared ordering service of the network; here the Global Synchronizer |
| Super Validator, DSO | the organisations that run the Global Synchronizer; the DSO party represents them on the ledger |
| Template, contract, choice | a contract type; one instance of it; an action that can be exercised on it |
| Interface | a common shape several templates implement, such as Holding for tokens |
| Daml package (DAR) | compiled contract code; must be installed on the validator before use |
| Offset | position in this validator's ledger |
| Update | one committed transaction (or other ledger change) |
| Traffic | the network resource every submission consumes, counted in bytes |
| Canton Coin (CC), Amulet | the network's native token; Amulet is its instrument ID |
| CIP-56, token standard | the common interfaces for tokens on Canton: holdings, transfers, allocations |
| Registry | the off-ledger service of a token issuer that supplies what a transfer needs |
| Transfer preapproval | a contract that lets a party receive Canton Coin without accepting each transfer |
| Scan | the Super Validators' public read service for network data |
Ready to build?
Get a project and your first party on the VNCapital validator.
