Ledger Service API
Complete REST + WebSocket reference for the Ledger Service and Canton JSON API v2.
:8080) is the recommended interface — it absorbs 8 Canton quirks and 6 external-signing
facts. The Canton JSON API v2 (/v2/*) is the raw participant
API — use for admin operations not yet exposed via the Ledger Service.
Ledger Service Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health |
Health check + Canton reachability |
| POST | /api/v1/commands/submit |
Submit a Canton command (custodial, wraps /v2/commands/submit-and-wait) |
| GET | /api/v1/contracts/:module/:entity |
Fetch active contracts via /v2/state/active-contracts |
| GET | /api/v1/prices?ticker=BTC/USDC |
Latest published price for a ticker |
| POST | /api/v1/external/onboard |
External-party onboarding step 1: send pubkey + partyHint |
| POST | /api/v1/external/onboard/complete |
External-party onboarding step 2: submit signed topology |
| POST | /api/v1/commands/prepare |
Interactive submission: prepare transaction for external signing |
| POST | /api/v1/commands/execute |
Interactive submission: execute with wallet signature |
| WS | /api/v1/stream |
Real-time contract events (incremental /v2/updates cursor) |
Endpoint Details
GET /api/v1/health
Returns service status and Canton connectivity. No auth required.
curl -s http://localhost:8080/api/v1/health | jq
# Response 200:
{
"status": "ok",
"canton": "connected",
"uptime": "12s"
}
POST /api/v1/commands/submit
Submit a custodial Canton command. The service wraps it into the Canton
CreateAndExerciseCommand format, adds commandId, userId,
and actAs. Accepts the logical command shape — not raw Canton bodies.
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | Yes | Module.Entity short form (e.g. OptionIndex.Core.SplitRequest) |
payload | object | Yes | Template create arguments |
choice | string | No | Choice name to exercise on create (for CreateAndExercise) |
choiceArgument | object | No | Arguments for the post-create choice |
contractId | string | No | Exercise an existing contract (instead of creating) |
# Create a SplitRequest
curl -s -X POST \
"http://localhost:8080/api/v1/commands/submit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"templateId": "OptionIndex.Core.SplitRequest",
"payload": {
"depositor": "alice::1220abcd...",
"vaultAdmin": "alice::1220abcd...",
"amount": "100.0",
"strike": "50.0",
"maturity": "2027-01-01T00:00:00Z",
"ticker": "BTC/USDC",
"baseAsset": "ETH",
"quoteAsset": "USDC",
"oracle": "oracle::1220def0...",
"admin": "admin::1220cafe...",
"settlementDelaySeconds": "0",
"refCid": null
},
"choice": "Execute",
"choiceArgument": {}
}' | jq
GET /api/v1/contracts/:module/:entity
Read active contracts of a specific template. The service reads as the authenticated user's
party via /v2/state/active-contracts. Uses the upgrade-transparent package-name
reference internally.
| Parameter | Description | Example |
|---|---|---|
:module | DAML module name | OptionIndex.Oracle |
:entity | Template entity name | PublishedPrice |
# Query active PublishedPrice contracts
curl -s \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/v1/contracts/OptionIndex.Oracle/PublishedPrice" \
| jq
# Response 200:
{
"contracts": [
{
"contractId": "#1220:0",
"templateId": "#option-index-tracker-v3:OptionIndex.Oracle:PublishedPrice",
"payload": {
"publisher": "oracle::1220def0...",
"publicReader": "medici-price-feed::12206b02...",
"ticker": "BTC/USDC",
"price": "3142.58",
"timestamp": "2026-06-30T12:00:00Z"
},
"signatories": ["oracle::1220def0..."],
"observers": ["medici-price-feed::12206b02..."]
}
]
}
GET /api/v1/prices?ticker=BTC/USDC
Fetch the latest published price for a ticker. Reads as the oracle party
(or PUBLIC_PRICE_PARTY if configured).
curl -s \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/v1/prices?ticker=BTC/USDC" \
| jq
# Response 200:
{
"ticker": "BTC/USDC",
"price": "3142.58",
"timestamp": "2026-06-30T12:00:00Z",
"contractId": "#1220:0",
"publisher": "oracle::1220def0..."
}
POST /api/v1/external/onboard
Step 1 of external-party onboarding (Path C, non-custodial). The service initiates the
topology dance: NamespaceDelegation, PartyToKeyMapping, PartyToParticipantMapping.
On success returns {publicKeyHash, topologyRequest, multiHash} for the wallet
to sign.
# Step 1: Initiate onboarding
curl -s -X POST \
"http://localhost:8080/api/v1/external/onboard" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"publicKey": "MCowBQYDK2VwAyEA...",
"partyHint": "alice-wallet"
}' | jq
POST /api/v1/external/onboard/complete
Step 2 of external-party onboarding. Submit the wallet's signature over the
multiHash to finalize topology and allocate the external party.
# Step 2: Complete onboarding with signature
curl -s -X POST \
"http://localhost:8080/api/v1/external/onboard/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"publicKey": "MCowBQYDK2VwAyEA...",
"partyHint": "alice-wallet",
"signature": "base64-encoded-ed25519-signature",
"topologyRequest": { "... topology from step 1 ..." }
}' | jq
POST /api/v1/commands/prepare
Prepare a transaction for external (CIP-103 wallet) signing. Same command shape as
/submit, but returns {preparedTransaction, preparedTransactionHash, summary,
hashingSchemeVersion} instead of submitting. The wallet signs
preparedTransactionHash (base64-decode -> Ed25519 sign -> base64-encode).
curl -s -X POST \
"http://localhost:8080/api/v1/commands/prepare" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"templateId": "OptionIndex.Core.SplitRequest",
"payload": { "...same as submit..." },
"choice": "Execute",
"choiceArgument": {}
}' | jq
POST /api/v1/commands/execute
Submit a signed external-party transaction. Takes the prepare output plus the wallet's signature.
curl -s -X POST \
"http://localhost:8080/api/v1/commands/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"preparedTransaction": { "...from prepare..." },
"signature": "base64-encoded-ed25519-signature",
"signedBy": "alice-wallet::1220abcd...",
"actAs": ["alice-wallet::1220abcd..."],
"hashingSchemeVersion": "V1"
}' | jq
WS /api/v1/stream
WebSocket for real-time contract events. Uses incremental /v2/updates cursor
internally — seeded from current state, then streams only new events. Clients never build
raw Canton update bodies.
# Connect via websocat or browser WebSocket
websocat ws://localhost:8080/api/v1/stream \
-H "Authorization: Bearer $TOKEN"
# Each message is a JSON event:
# {"type":"created","contractId":"...","templateId":"...","payload":{...}}
Canton JSON API v2 (Direct)
For admin operations not yet exposed via the Ledger Service, use Canton directly:
| Method | Path | Purpose |
|---|---|---|
| POST | /v2/state/active-contracts |
Query current-state active contracts (bounded set, 413-safe) |
| GET | /v2/updates |
Incremental event stream (needs beginExclusive + filters) |
| POST | /v2/commands/submit-and-wait |
Submit a command, wait for commit |
| GET | /v2/parties |
List allocated parties |
| POST | /v2/parties |
Allocate a new party |
| GET | /v2/users |
List Canton users |
| POST | /v2/users |
Create a Canton user with rights |
| GET | /v2/packages |
List uploaded DAR packages |
| POST | /v2/packages |
Upload a DAR |
Error Format
All errors follow a consistent JSON structure. The Ledger Service normalizes Canton's sometimes-HTTP-200 errors into proper 4xx/5xx responses.
{
"error": {
"code": "PERMISSION_DENIED",
"message": "User does not have canActAs rights for party alice::1220abcd...",
"details": {
"cantonStatus": 403,
"traceId": "abc123def456"
}
}
}
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_ARGUMENT | Malformed request body or missing required field |
| 401 | UNAUTHENTICATED | Missing, expired, or mis-issued token |
| 403 | PERMISSION_DENIED | Token valid but Canton user lacks rights |
| 404 | NOT_FOUND | Contract, party, or user not found |
| 409 | ALREADY_EXISTS | Duplicate party hint or user id |
| 413 | CONTENT_TOO_LARGE | Response too large — use active-contracts, not /v2/updates from zero |
| 500 | INTERNAL | Unexpected server or Canton error |
| 503 | UNAVAILABLE | Canton unreachable |
Environment Configuration
| Env Var | Default | Purpose |
|---|---|---|
LISTEN_ADDR | :8080 | HTTP listen address |
LEDGER_URL | https://canton.dev.medici.loan | Canton JSON API base URL |
KEYCLOAK_URL | https://keycloak.dev.medici.loan | Keycloak base URL |
KEYCLOAK_CLIENT_SECRET | — | app-user-validator client secret |
PACKAGE_ID | — | DAML package content hash (64 hex chars) |
ORACLE_PARTY | — | Signing oracle party |
PUBLIC_PRICE_PARTY | — | Dedicated public reader party |
CANTON_TOKEN | — | HS256 admin token (direct-token mode) |
CANTON_TOKEN_ENABLED | false | Bypass Keycloak, use CANTON_TOKEN |
LOG_LEVEL | info | Zerolog level: debug, info, warn, error |
Template Reference
Package: option-index-tracker-v3 (DAML SDK 3.5.1, version 0.1.5)
Package reference: #option-index-tracker-v3 (upgrade-transparent package-name form)
| Module | Templates |
|---|---|
OptionIndex.Core |
CollateralVault, PToken, NToken,
SplitRequest, CollateralRelease, SettlementClaim,
SwapOffer |
OptionIndex.Oracle |
PriceObservation, PublishedPrice, OracleAuthorization,
LazyOracleRequest, MultiAttestationSession, OracleConfig |
OptionIndex.Rebalancing |
RebalanceOffer, RebalanceReceipt, RolloverService,
GradualRebalanceAuction, AtomicRollRequest, AtomicRollReceipt,
QuoteRequest, QuoteResponse, RollHistory |
OptionIndex.Governance |
AdminRegistry, GovernanceProposal, ProposalAcceptance,
ProposalRejection, AdminTransfer, CircuitBreaker,
CommissionConfig, CommissionClaim, VersionRegistry,
MigrationRegistry, PoolMaintainer, SystemSummary |
OptionIndex.Perpetual |
PerpetualVault, PerpetualPToken, PerpetualNToken |
OptionIndex.PhysicalSettlement |
PhysicalVault, PhysicalPToken, PhysicalNToken,
ExerciseReceipt, QuoteClaim, PhysicalCollateralRelease,
PhysicalSplitRequest |
OptionIndex.PoolVault |
PoolVault, PoolPToken, PoolNToken,
PoolExerciseReceipt, PoolRecombinationReceipt, PoolRedemption |
OptionIndex.Intents |
IntentAnnouncement, VaultComplianceRegistry |
curl Recipe Reference
# 1. Health check
curl -s http://localhost:8080/api/v1/health | jq
# 2. Get a price
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/v1/prices?ticker=BTC/USDC" | jq
# 3. Query active contracts
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/v1/contracts/OptionIndex.Oracle/PublishedPrice" | jq
# 4. Submit a command (custodial)
curl -s -X POST \
"http://localhost:8080/api/v1/commands/submit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"templateId":"OptionIndex.Core.SplitRequest","payload":{...}}' | jq
# 5. Prepare for external signing (non-custodial)
curl -s -X POST \
"http://localhost:8080/api/v1/commands/prepare" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"templateId":"OptionIndex.Core.SplitRequest","payload":{...}}' | jq
# 6. Query Canton directly (parties)
curl -s \
"https://canton.dev.medici.loan/v2/parties" \
-H "Authorization: Bearer $TOKEN" | jq
# 7. Allocate a party (direct Canton, admin)
curl -s -X POST \
"https://canton.dev.medici.loan/v2/parties" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"partyHint":"my-bot","identityProviderId":"keycloak-appuser"}' | jq