Medici Protocol

Ledger Service API

Complete REST + WebSocket reference for the Ledger Service and Canton JSON API v2.

Two API surfaces: The Ledger Service (REST + WebSocket on :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

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

FieldTypeRequiredDescription
templateIdstringYesModule.Entity short form (e.g. OptionIndex.Core.SplitRequest)
payloadobjectYesTemplate create arguments
choicestringNoChoice name to exercise on create (for CreateAndExercise)
choiceArgumentobjectNoArguments for the post-create choice
contractIdstringNoExercise 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.

ParameterDescriptionExample
:moduleDAML module nameOptionIndex.Oracle
:entityTemplate entity namePublishedPrice
# 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:

MethodPathPurpose
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"
    }
  }
}
HTTPCodeMeaning
400INVALID_ARGUMENTMalformed request body or missing required field
401UNAUTHENTICATEDMissing, expired, or mis-issued token
403PERMISSION_DENIEDToken valid but Canton user lacks rights
404NOT_FOUNDContract, party, or user not found
409ALREADY_EXISTSDuplicate party hint or user id
413CONTENT_TOO_LARGEResponse too large — use active-contracts, not /v2/updates from zero
500INTERNALUnexpected server or Canton error
503UNAVAILABLECanton unreachable

Environment Configuration

Env VarDefaultPurpose
LISTEN_ADDR:8080HTTP listen address
LEDGER_URLhttps://canton.dev.medici.loanCanton JSON API base URL
KEYCLOAK_URLhttps://keycloak.dev.medici.loanKeycloak base URL
KEYCLOAK_CLIENT_SECRETapp-user-validator client secret
PACKAGE_IDDAML package content hash (64 hex chars)
ORACLE_PARTYSigning oracle party
PUBLIC_PRICE_PARTYDedicated public reader party
CANTON_TOKENHS256 admin token (direct-token mode)
CANTON_TOKEN_ENABLEDfalseBypass Keycloak, use CANTON_TOKEN
LOG_LEVELinfoZerolog 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)

ModuleTemplates
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