Quickstart
Your first API call in 5 minutes.
Prerequisites
- curl — for making HTTP requests
- jq — for parsing and pretty-printing JSON responses
- A terminal — any shell will do
- Network access to the Medici dev environment (canton.dev.medici.loan and keycloak.dev.medici.loan)
Dev credentials — These examples use the dev environment well-known credentials.
For production, use your own Keycloak client secret and token.
Step 1: Get a Keycloak Token
The Medici protocol uses Keycloak for identity. Every API call needs a Bearer token.
Request one with the client_credentials grant:
# Request a service-account token (RS256 JWT, 5-minute expiry) TOKEN=$(curl -s -X POST \ "https://keycloak.dev.medici.loan/realms/AppUser/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=medici-app" \ -d "client_secret=" \ | jq -r '.access_token') # Verify the token echo "Token length: ${#TOKEN} chars" echo "Token preview: ${TOKEN:0:50}..." # Decode the payload (middle segment) to inspect claims echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
Token expiry: RS256 tokens live 5 minutes. The Ledger Service auto-refreshes,
but if you are calling Canton directly you must refresh before expiry.
Step 2: Health Check
Verify the Ledger Service is up and connected to Canton:
curl -s "https://canton.dev.medici.loan/api/v1/health" | jq
# Expected response:
# {
# "status": "ok",
# "canton": "connected",
# "uptime": "12s"
# }
If you are using the Ledger Service (recommended), check its health endpoint:
# Ledger Service health (if running locally) curl -s "http://localhost:8080/api/v1/health" | jq
Step 3: Read Active Contracts
Query active contracts on the Canton ledger. This example reads PublishedPrice
contracts (the public price feed):
# Via Ledger Service (recommended — simpler, handles Canton quirks)
curl -s \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/v1/contracts/OptionIndex.Oracle/PublishedPrice" \
| jq
# Direct Canton query (advanced — raw /v2/state/active-contracts)
# Step 1: fetch a REAL ledger-end offset. `jq -e` aborts if it is missing, so you never
# issue the offset-less read below (that one returns HTTP 200 [] — see the warning).
OFFSET=$(curl -s "https://canton.dev.medici.loan/v2/state/ledger-end" \
-H "Authorization: Bearer $TOKEN" | jq -e .offset) || exit 1
# Step 2: read, filtering by a party you can readAs. PublishedPrice is public, so the
# medici-price-feed party works for any token that reads as it. Assign PARTY explicitly:
# left unset it expands to "", the body becomes "filtersByParty":{"":{...}}, and the read
# comes back empty for the same reason a missing offset does.
PARTY=medici-price-feed::YOUR_NAMESPACE_FINGERPRINT # full party id, from the medici-party-config ConfigMap
curl -s -X POST \
"https://canton.dev.medici.loan/v2/state/active-contracts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"activeAtOffset": '$OFFSET',
"verbose": true,
"filter": { "filtersByParty": { "'$PARTY'": { "cumulative": [
{ "identifierFilter": { "TemplateFilter": { "value": {
"templateId": "#option-index-tracker-v10:OptionIndex.Oracle:PublishedPrice" } } } }
]}}}
}' | jq 'if type=="array"
then .[].contractEntry.JsActiveContract.createdEvent
| (.payload // .createArguments // .createArgument)
else "ERROR BODY, not a result set: \(.cause // .)" end'
# The payload arrives under `payload`, `createArguments`, OR `createArgument` depending on
# the endpoint and build — read all three. A path pinned to one key prints `null` on a
# perfectly good read, which looks exactly like the empty result described below.
Empty results are ambiguous — read this before concluding "no contracts".
A response of
[] means "nothing this request could see", which has three
different causes that look identical:
- No
activeAtOffset— Canton answersHTTP 200 []rather than rejecting the body. Always fetch/v2/state/ledger-endfirst. - The filter used
templateIds— that is not a field of Canton'sFiltersschema. It is dropped, the filter decodes empty, and empty means match every template. Use thecumulativeform above. - Visibility — a token only sees contracts whose signatory is in
its
readAs. Confirm your party is a stakeholder before reporting absence.
jq 'length' on an error returns its key count (10 for a masked
403) instead of 0 — check type == "array" before counting.
Package ID note: The package reference uses the upgrade-transparent
package-name form (
#option-index-tracker-v10), not a content hash. This
ensures the reference never goes stale across DAR rebuilds.
Step 4: Submit a Command (Optional)
Create a SplitRequest to split collateral into P and N tokens
(requires a funded party on the ledger):
# Via Ledger Service — see /api/ for the full submit schema
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": "",
"vaultAdmin": "",
"amount": "100.0",
"strike": "50.0",
"maturity": "2027-01-01T00:00:00Z",
"ticker": "BTC/USDC",
"baseAsset": "ETH",
"quoteAsset": "USDC",
"oracle": "",
"admin": "",
"settlementDelaySeconds": "0",
"refCid": null
}
}' | jq
Next Steps
| Topic | Page |
|---|---|
| Understand authentication in depth | Authentication |
| Full API reference (all 13 endpoints) | Ledger Service API |
| Template field-by-field reference | DAML Templates |
| How the system fits together | Architecture |
| Learn the concepts behind the protocol | Learn |
| Strategies and intent-based trading | Strategy Engine |