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 URLhttps://canton.vncapital.io
NetworkCanton Network MainNet (Global Synchronizer)
SoftwareCanton 3.5.18, Splice 0.8.3
API styleJSON over HTTPS. HTTP/1.1 and HTTP/2
Guide version2026-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:

ItemExampleSecret?
Client IDproject-acmeno
Client secreta long random stringyes — keep it on your server
Ledger user ID11111111-2222-3333-4444-555555555555no
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):

Shell
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 send

The 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:

Shell
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.

Shell
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}}}}]}}}}')" | jq

Submitting 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#

PrefixWhatToken needed
/realms/customers/…Get a tokenclient ID + secret
/v2/…Canton Ledger API (JSON): your contracts, your transactionsyes
/api/scan/…, /registry/…Public network data and the Canton Coin token registryyes

Anything else answers 404 Not found.

Identifiers you will see#

ThingLooks likeNotes
Party IDalice::1220<64 hex>URL-encode it in paths: :: becomes %3A%3A
Synchronizer IDglobal-domain::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19acccthe Global Synchronizer; read it from /v2/state/connected-synchronizers
DSO partyDSO::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19acccthe network's own party; read it from /api/scan/v0/dso-party-id
Contract IDlong hex string starting with 00
Update IDhex string starting with 1220one committed transaction
Offsetinteger, e.g. 5554084position in this validator's ledger; only grows
Template ID#package-name:Module.Name:Templatewrite 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.

Shell
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"
JSON
{
  "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_type is always the literal client_credentials.

Inside the token (a signed JWT, RS256):

ClaimValue
isshttps://canton.vncapital.io/realms/customers
audhttps://canton.network.global
subyour ledger user ID
azpyour client ID
expexpiry, seconds since 1970

4.2 Public keys and discovery (optional)#

For libraries that configure themselves. No token needed.

RouteReturns
GET /realms/customers/.well-known/openid-configurationissuer, token_endpoint, jwks_uri, …
GET /realms/customers/protocol/openid-connect/certsthe 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:

Shell
A="Authorization: Bearer $TOKEN"; J='Content-Type: application/json'

5.1 About you and the node#

RouteReturns
GET /v2/versionnode version and feature flags
GET /v2/authenticated-useryour ledger user
GET /v2/users/{your-user-id}the same, by ID (other users: 403)
GET /v2/users/{your-user-id}/rightswhat your user may do
GET /v2/state/ledger-endnewest offset
GET /v2/state/connected-synchronizersthe synchronizer ID
GET /v2/parties/participant-idthe hosting validator's ID
Shell
curl -s -H "$A" $BASE/v2/authenticated-user
JSON
{"user": {"id": "11111111-2222-3333-4444-555555555555", "primaryParty": "", "isDeactivated": false,
          "metadata": {"resourceVersion": "0", "annotations": {}}, "identityProviderId": "customers",
          "primaryPartyAuthentication": false}}
Shell
curl -s -H "$A" $BASE/v2/users/$USER_ID/rights
JSON
{"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.

Shell
curl -s -H "$A" $BASE/v2/state/ledger-end              # {"offset": 5554084}
curl -s -H "$A" $BASE/v2/state/connected-synchronizers
JSON
{"connectedSynchronizers": [{"synchronizerAlias": "global",
  "synchronizerId": "global-domain::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc",
  "permission": "PARTICIPANT_PERMISSION_UNSPECIFIED"}]}
Shell
curl -s -H "$A" $BASE/v2/parties/participant-id
JSON
{"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.

Shell
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:

JSON
[{
  "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:

FilterSelectsBody
Wildcardeverything the party can see{"identifierFilter":{"WildcardFilter":{"value":{"includeCreatedEventBlob":false}}}}
Templateone template{"identifierFilter":{"TemplateFilter":{"value":{"templateId":"#splice-amulet:Splice.AmuletRules:TransferPreapproval","includeCreatedEventBlob":false}}}}
Interfaceevery 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: true adds field names to createArgument.
  • ?limit=N on 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).

Shell
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}}}}]}}}}}}')"
JSON
{"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"}}]
}}}}
transactionShapeYou get
TRANSACTION_SHAPE_ACS_DELTAcontracts created and archived (enough to keep a local copy of your contracts)
TRANSACTION_SHAPE_LEDGER_EFFECTSadditionally 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

Shell
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.

Shell
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}}}}]}}}}}}')"
JSON
{"updates": [{"update": {"Transaction": {"value": {"updateId": "1220…", "offset": 5553915, "events": [ … ]}}}}],
 "lowestPageOffsetExclusive": 5553914,
 "highestPageOffsetInclusive": 5553918,
 "nextPageToken": "…"}

A simple follow loop (verified):

  1. Remember the offset you have processed up to. Start with the current ledger-end, or 0 for the full history.
  2. Call with beginOffsetExclusive = that offset.
  3. Process updates (it may be empty). Set your offset to highestPageOffsetInclusive: the page covers everything up to and including it, so nothing is skipped even when maxPageSize cut the page short.
  4. If updates was 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.

RouteReturns
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-packagesthe same for several package names at once
Shell
curl -s -H "$A" "$BASE/v2/interactive-submission/preferred-package-version?package-name=splice-amulet&parties=$(jq -rn --arg p "$PARTY" '$p|@uri')"
JSON
{"packagePreference": {"packageReference": {"packageId": "fb10433a48c2…", "packageName": "splice-amulet", "packageVersion": "0.1.22"},
                       "synchronizerId": "global-domain::1220…"}}
Shell
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.

Code
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 MainNet

Rules:

  • 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

Shell
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
FieldMeaning
userIdyour ledger user ID
commandIdyour own unique string for this command; reuse it only to retry the very same command
actAsthe party that signs. One of your parties
readAsother parties of yours whose contracts the command needs to read; usually []
synchronizerIdthe Global Synchronizer ID (required)
packageIdSelectionPreferenceusually [] (required)
verboseHashingtrue adds hashingDetails: a readable listing of exactly what is hashed
commandsexactly one command, see below
disclosedContractscontracts the command needs that your party cannot see itself (used for coin, section 7)

Commands:

JSON
{"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:

JSON
{
  "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 preparedTransaction and recomputes the hash itself instead of trusting the node; the Canton Wallet SDK does this. verboseHashing: true shows 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:

Shell
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):

Python
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):

JavaScript
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.

Shell
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"}]}]}
}')"
JSON
{"updateId": "12201d11b890b3e4eeeca9c55bbc0043e7e71ffbc1b30ac8ed097e18e4a8f232bd48", "completionOffset": 5553915}

The transaction is now on MainNet. Read it back with 5.3.

VariantBehaviour
/v2/interactive-submission/executeAndWaitwaits; returns updateId and completionOffset. Used throughout this guide
/v2/interactive-submission/executeAndWaitForTransactionwaits; returns the whole transaction. Takes an optional transactionFormat (same shape as in 5.3). Not run by us
/v2/interactive-submission/executereturns at once without waiting; find the result later by polling. Not run by us

A wrong signature:

JSON
{"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:

Shell
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):

PurposeInterface 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#

Shell
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:

JSON
{"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:

JSON
{"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#

  1. Ask the registry for the context of the acceptance:
Shell
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
JSON
{"choiceContextData": {"values": {"amulet-rules": {…}, "open-round": {…}, "external-party-config-state": {…}, "expire-lock": {…}}},
 "disclosedContracts": [{"templateId": "…", "contractId": "00…", "createdEventBlob": "…", "synchronizerId": "global-domain::1220…"}, …]}
  1. Prepare the acceptance, passing the context and the disclosed contracts through unchanged:
Shell
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
  1. 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#

  1. Collect the contract IDs of your unlocked holdings of the token (7.1).
  2. Ask the registry for the transfer factory:
Shell
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
JSON
{"factoryId": "00…",
 "transferKind": "direct",
 "choiceContext": {"choiceContextData": {"values": { … }}, "disclosedContracts": [ … ]}}
transferKindWhat will happen
directthe receiver has a transfer preapproval: the coin arrives in this one transaction
offerthe receiver must accept (7.3) before executeBefore; until then your holdings are locked
selfsender and receiver are the same party (merging or splitting holdings)
  1. Prepare the transfer: the same arguments, with the registry's context filled in.
Shell
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
  1. Sign and execute (6.2, 6.3).

Notes:

  • amount may 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/withdraw and TransferInstruction_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:

Shell
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:

Shell
curl -s -H "$A" "$BASE/api/scan/v0/transfer-preapprovals/by-party/$(jq -rn --arg p "$PARTY" '$p|@uri')"
JSON
{"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_Cancel on 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: GET and POST under /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/scans lists them).
  • This is network-wide public data. It is not the place to read your own contracts: use section 5.

8.1 Network facts#

RouteReturns
GET /api/scan/v0/dso-party-id{"dso_party_id": "DSO::1220…"}
GET /api/scan/v0/dsothe network's governing rules and the Super Validators (large, about 70 KB)
GET /api/scan/v0/splice-instance-namesdisplay names: "Canton Network", "Canton Coin", "CC", "Canton Name Service"
GET /api/scan/v0/scansall Scan instances with their public addresses
GET /api/scan/v0/dso-sequencersthe network's sequencers

8.2 Canton Coin rules and rounds#

RouteBodyReturns
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-roundsrecently 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#

RouteReturns
GET /api/scan/v0/domains/{synchronizer-id}/parties/{party}/participant-idwhich 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-appsall 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-statusa 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=Nvalidator licences on the network

Party and synchronizer IDs in paths must be URL-encoded:

Shell
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#

RouteReturns
GET /api/scan/v0/ans-entries?page_size=Nname 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#

RouteBody / queryReturns
POST /api/scan/v2/updates{"page_size": 10}; continue with "after": {"after_migration_id": M, "after_record_time": "…"} taken from the last itemthe 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=4the 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.

Shell
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]}')"
JSON
{"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#

RouteReturns
GET /registry/metadata/v1/info{"adminId": "DSO::1220…", "supportedApis": {…}}
GET /registry/metadata/v1/instrumentsthe instruments of this registry
GET /registry/metadata/v1/instruments/AmuletCanton Coin: name, symbol CC, 10 decimals, total supply, supported token APIs
POST /registry/transfer-instruction/v1/transfer-factorystart a transfer (7.4)
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/acceptcontext to accept (7.3)
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/rejectcontext to reject
POST /registry/transfer-instruction/v1/{contract-id}/choice-contexts/withdrawcontext 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#

CodeMeaningWhat to do
200success
400the request is wrong: missing field, bad value, invalid signature, refused commandread the message; fix the request
401no valid token: missing on a Scan/registry route, expired, malformed, wrong signatureget a new token
403the token is valid but may not do this: another party's data, another user, a project that is not activecheck the party and your rights
404the 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
413body larger than 2 MB
429rate limit reached. The answer has a Retry-After header in secondswait that long, then retry
502 / 503temporarily unavailableretry after a few seconds

9.2 Error bodies#

From the service itself (token, limits, availability):

JSON
{"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:

JSON
{"code": "INVALID_ARGUMENT", "cause": "The submitted request has invalid arguments: …",
 "correlationId": "…", "traceId": "…", "context": {}, "resources": [], "errorCategory": 8, "grpcCodeValue": 3}
  • code and cause say what happened. Quote traceId when asking for support.
  • Permission problems are deliberately vague: "cause": "A security-sensitive error has been received" with HTTP 403 (grpcCodeValue 7). 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#

LimitDefault
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 address60 per minute, bursts of 20
Request body2 MB
Token lifetime5 minutes
Commands per transaction1

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 commandId is in the transaction), or retry with the same commandId and submissionId.
  • After a 429 wait for Retry-After. Do not retry in a tight loop.

10. What is not available#

Not availableInstead
Creating users or parties through the APIdone by VNCapital when your project is set up
Installing Daml packages yourselfsend 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 streamspoll get-updates-page (5.5)
Listing other users, parties or identity settings—
The validator's own wallet and administration API—
A test networkthis service is MainNet: every transaction is real and permanent. Develop against a local Canton network first
Several commands in one transactionone 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.

Python
#!/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

MethodRouteTPurpose
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

MethodRouteTPurpose
GET/v2/versionTnode version
GET/v2/authenticated-userTyour user
GET/v2/users/{user-id}Tyour user by ID
GET/v2/users/{user-id}/rightsTyour rights
GET/v2/state/ledger-endTnewest offset
GET/v2/state/connected-synchronizersTsynchronizer ID
GET/v2/parties/participant-idTvalidator ID (= operator party)
POST/v2/state/active-contractsTcurrent contracts
POST/v2/updates/update-by-idTone transaction by ID
POST/v2/updates/update-by-offsetTone transaction by offset
POST/v2/updates/get-updates-pageThistory / follow new events
POST/v2/updates/flats, /trees, /transaction-by-id, /transaction-by-offsetTdeprecated equivalents
POST/v2/events/events-by-contract-idTcreate/archive events of one contract
GET/v2/packagesTinstalled packages
GET/v2/packages/{package-id}Tdownload a package
GET/v2/packages/{package-id}/statusTpackage status
GET/v2/interactive-submission/preferred-package-versionTpackage version for a party
POST/v2/interactive-submission/preferred-packagesTpackage versions for several names
GET/docs/openapiTfull Ledger API description

Ledger API — write (signed by your key)

MethodRouteTPurpose
POST/v2/interactive-submission/prepareTbuild a transaction and its hash (writes nothing)
POST/v2/interactive-submission/executeAndWaitTsubmit a signed transaction and wait
POST/v2/interactive-submission/executeAndWaitForTransactionTsame, returns the transaction
POST/v2/interactive-submission/executeTsubmit without waiting
POST/v2/parties/external/generate-topologyTdraft of a party registration (writes nothing; registration itself is completed by VNCapital)

Network data and token registry

MethodRouteTPurpose
GET, POST/api/scan/v0/…, /api/scan/v2/…Tpublic network data (section 8)
GET, POST/registry/…TCanton Coin token registry (sections 7 and 8.6)

13. Glossary#

TermMeaning
Partyan identity on the ledger that owns contracts and signs transactions. name::fingerprint
Self-custody (external) partya 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
Synchronizerthe shared ordering service of the network; here the Global Synchronizer
Super Validator, DSOthe organisations that run the Global Synchronizer; the DSO party represents them on the ledger
Template, contract, choicea contract type; one instance of it; an action that can be exercised on it
Interfacea common shape several templates implement, such as Holding for tokens
Daml package (DAR)compiled contract code; must be installed on the validator before use
Offsetposition in this validator's ledger
Updateone committed transaction (or other ledger change)
Trafficthe network resource every submission consumes, counted in bytes
Canton Coin (CC), Amuletthe network's native token; Amulet is its instrument ID
CIP-56, token standardthe common interfaces for tokens on Canton: holdings, transfers, allocations
Registrythe off-ledger service of a token issuer that supplies what a transfer needs
Transfer preapprovala contract that lets a party receive Canton Coin without accepting each transfer
Scanthe Super Validators' public read service for network data

Ready to build?

Get a project and your first party on the VNCapital validator.