# VNCapital Validator — 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.

---

## Contents

1. [What you get](#1-what-you-get)
2. [Five-minute start](#2-five-minute-start)
3. [Basics: addresses, identifiers, conventions](#3-basics)
4. [Authentication](#4-authentication)
5. [Reading from the ledger](#5-reading-from-the-ledger)
6. [Submitting transactions: prepare, sign, execute](#6-submitting-transactions)
7. [Canton Coin and other tokens](#7-canton-coin-and-other-tokens)
8. [Network data (Scan) and the token registry](#8-network-data-scan-and-the-token-registry)
9. [Errors and limits](#9-errors-and-limits)
10. [What is not available](#10-what-is-not-available)
11. [Complete example in Python](#11-complete-example-in-python)
12. [All routes at a glance](#12-all-routes-at-a-glance)
13. [Glossary](#13-glossary)

---

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

```bash
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:

```bash
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`.

```bash
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](#6-submitting-transactions).

---

## 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](#55-following-new-events-polling)).

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

```bash
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):

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

```bash
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 |

```bash
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}}
```

```bash
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.

```bash
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"}]}
```

```bash
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](#75-receive-directly-transfer-preapproval)).

### 5.2 Active contracts
`POST /v2/state/active-contracts` — the contracts that exist for your party at a given offset.

```bash
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`:

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

```bash
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"}}]
}}}}
```

| `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`

```bash
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.

```bash
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.

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

```bash
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…"}}
```
```bash
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 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`

```bash
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](#7-canton-coin-and-other-tokens)) |

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:
```bash
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):
```js
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.

```bash
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](#53-one-transaction).

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

```bash
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](#8-network-data-scan-and-the-token-registry)).
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
```bash
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:

```bash
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…"}, …]}
```

2. Prepare the acceptance, passing the context and the disclosed contracts through unchanged:

```bash
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
```
3. 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:

```bash
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": [ … ]}}
```

| `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) |

3. Prepare the transfer: the same arguments, with the registry's context filled in.

```bash
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
```
4. 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:

```bash
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:

```bash
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
| 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:
```bash
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.

```bash
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
| 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):
```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
| 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](#55-following-new-events-polling) (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 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`.

```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**

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