Intent API Reference
Complete reference for the 9 Strategy Intent REST endpoints exposed by the Ledger Service.
All endpoints live under /api/v1/.
https://canton.int.medici.loan (int environment).
All requests require a bearer token obtained from Keycloak unless marked "no auth".
The token's sub claim is used as the intent owner for authorization checks.
POST /api/v1/intents
Create a new strategy intent. Returns a fully-populated StrategyIntent with auto-generated id, status: "active", and owner set from the JWT sub claim.
Request body
| Field | Type | Required | Constraints |
|---|---|---|---|
type | string | yes | treasury | hedge | leverage | yield | custom |
ticker | string | yes | ^[A-Z]{2,5}/[A-Z]{2,5}$ (e.g. "BTC/USDC") |
collateral.asset | string | yes | USDC | WETH | WBTC |
collateral.amount | string | yes | Decimal string, e.g. "50000.00" |
config.strikeRatio | number | yes | 0.01 – 1.0 |
config.maturityDays | integer | yes | 1 – 365 |
config.rebalanceThreshold | number | yes | >= 1.0 |
config.autoRoll | boolean | no | Defaults to template value if omitted |
config.rollStrategy | string | yes | standard | shortDated | manual |
constraints.maxAnnualDriftBps | integer | no | 0 – 10,000 |
constraints.maxSlippagePerRollBps | integer | no | 0 – 100 |
constraints.maxCapital | string | yes | Decimal string |
constraints.stopLossDriftBps | integer | no | 0 – 10,000 |
notifications.onRoll | boolean | no | |
notifications.onThresholdBreach | boolean | no | |
notifications.onStopLoss | boolean | no | |
notifications.channels | string[] | yes | At least 1 from: email, push, inapp, webhook |
owner field,
if present in the JSON payload, is silently discarded. The handler always sets it from
the JWT sub claim. This prevents impersonation.
Response 201
{
"intent": {
"id": "intent-1719763200000-a1b2c3d4",
"owner": "alice",
"type": "treasury",
"status": "active",
"ticker": "BTC/USDC",
"collateral": { "asset": "USDC", "amount": "50000.00" },
"config": {
"strikeRatio": 0.50,
"maturityDays": 30,
"rebalanceThreshold": 1.5,
"autoRoll": true,
"rollStrategy": "standard"
},
"constraints": {
"maxAnnualDriftBps": 200,
"maxSlippagePerRollBps": 10,
"maxCapital": "50000.00",
"stopLossDriftBps": 500
},
"notifications": {
"onRoll": true,
"onThresholdBreach": true,
"onStopLoss": true,
"channels": ["inapp", "webhook"]
},
"runtime": null,
"createdAt": "2026-06-30T12:00:00Z",
"updatedAt": "2026-06-30T12:00:00Z"
},
"fetchedAt": "2026-06-30T12:00:00Z"
}
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_request | Body is not valid JSON |
| 400 | validation_failed | Schema validation or sanitization fails. message lists the specific failures. |
| 401 | unauthorized | Missing or invalid bearer token |
| 500 | creation_failed | Store-level failure |
cURL
curl -s -X POST "$BASE/api/v1/intents" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "treasury",
"ticker": "BTC/USDC",
"collateral": {"asset": "USDC", "amount": "50000.00"},
"config": {
"strikeRatio": 0.50,
"maturityDays": 30,
"rebalanceThreshold": 1.5,
"autoRoll": true,
"rollStrategy": "standard"
},
"constraints": {
"maxAnnualDriftBps": 200,
"maxSlippagePerRollBps": 10,
"maxCapital": "50000.00",
"stopLossDriftBps": 500
},
"notifications": {
"onRoll": true,
"onThresholdBreach": true,
"onStopLoss": true,
"channels": ["inapp", "webhook"]
}
}' | jq .
GET /api/v1/intents
List all strategy intents owned by the authenticated user. Optional ?status= query parameter filters by lifecycle state.
Query parameters
| Parameter | Type | Required | Values |
|---|---|---|---|
status | string | no | active, paused, completed, cancelled, error |
Response 200
{
"intents": [
{
"id": "intent-1719763200000-a1b2c3d4",
"owner": "alice",
"type": "treasury",
"status": "active",
"ticker": "BTC/USDC",
"collateral": { "asset": "USDC", "amount": "50000.00" },
"config": { ... },
"constraints": { ... },
"notifications": { ... },
"runtime": {
"vaultIds": ["vault-abc123"],
"currentDriftBps": 150,
"totalRolls": 3,
"totalCostBps": 24,
"lastRollAt": "2026-06-28T14:00:00Z",
"lastRollCostBps": 8
},
"createdAt": "2026-06-30T12:00:00Z",
"updatedAt": "2026-06-30T12:00:00Z"
}
],
"count": 1,
"fetchedAt": "2026-06-30T12:00:00Z"
}
Errors
| Status | error | When |
|---|---|---|
| 401 | unauthorized | Missing or invalid bearer token |
| 500 | list_failed | Store-level failure |
cURL
# All intents curl -s "$BASE/api/v1/intents" \ -H "Authorization: Bearer $TOKEN" | jq . # Active only curl -s "$BASE/api/v1/intents?status=active" \ -H "Authorization: Bearer $TOKEN" | jq . # Cancelled curl -s "$BASE/api/v1/intents?status=cancelled" \ -H "Authorization: Bearer $TOKEN" | jq .
GET /api/v1/intents/:id
Get a single strategy intent by its ID. Returns the full intent including runtime state if populated.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Intent ID, format: intent-{unixms}-{8hex} |
Response 200
{
"intent": { /* full StrategyIntent — same shape as create response */ },
"fetchedAt": "2026-06-30T12:00:00Z"
}
Errors
| Status | error | When |
|---|---|---|
| 400 | missing_id | Empty :id parameter |
| 403 | forbidden | Caller is not the intent owner |
| 404 | not_found | No intent with this ID exists |
cURL
curl -s "$BASE/api/v1/intents/intent-1719763200000-a1b2c3d4" \ -H "Authorization: Bearer $TOKEN" | jq .
PUT /api/v1/intents/:id
Update mutable fields on an intent. Only config, constraints, and notifications are recognized — all other top-level keys are silently ignored. Each field within a recognized key is only updated if present in the request.
completed
or cancelled cannot be updated — the handler returns HTTP 500 with message
"intent {id} is completed/cancelled and cannot be modified".
Request body
Any subset of these keys. Fields not present are left unchanged. Unknown top-level keys are silently ignored.
| Key | Type | Recognized sub-fields |
|---|---|---|
config | object (partial) | strikeRatio, maturityDays, rebalanceThreshold, autoRoll, rollStrategy |
constraints | object (partial) | maxAnnualDriftBps, maxSlippagePerRollBps, maxCapital, stopLossDriftBps |
notifications | object (partial) | onRoll, onThresholdBreach, onStopLoss, channels |
Response 200
Same shape as GET — full StrategyIntent after the update is applied.
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_request | Body is not valid JSON |
| 400 | missing_id | Empty :id parameter |
| 401 | unauthorized | Missing or invalid bearer token |
| 403 | update_failed | Caller is not the intent owner |
| 404 | not_found | No intent with this ID exists |
| 500 | update_failed | Store-level failure, or intent is in a terminal state |
cURL
curl -s -X PUT "$BASE/api/v1/intents/intent-1719763200000-a1b2c3d4" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"strikeRatio": 0.55,
"maturityDays": 45
},
"constraints": {
"maxAnnualDriftBps": 300
}
}' | jq .
POST /api/v1/intents/:id/pause
Pause an active intent. Execution is suspended — vaults remain in place but no rolls or rebalances occur. Only intents in active status can be paused.
Response 200
{
"intent": {
"id": "intent-1719763200000-a1b2c3d4",
"status": "paused",
...
},
"fetchedAt": "2026-06-30T12:00:00Z"
}
Errors
| Status | error | When |
|---|---|---|
| 403 | pause_failed | Caller is not the intent owner |
| 404 | pause_failed | Intent not found |
| 409 | pause_failed | Intent status is not active (e.g. already paused, completed, cancelled) |
| 500 | pause_failed | Store-level failure |
cURL
curl -s -X POST "$BASE/api/v1/intents/intent-1719763200000-a1b2c3d4/pause" \ -H "Authorization: Bearer $TOKEN" | jq .
POST /api/v1/intents/:id/resume
Resume a paused intent. Execution restarts from where it left off. Only intents in paused status can be resumed.
Response 200
{
"intent": {
"id": "intent-1719763200000-a1b2c3d4",
"status": "active",
...
},
"fetchedAt": "2026-06-30T12:00:00Z"
}
Errors
| Status | error | When |
|---|---|---|
| 403 | resume_failed | Caller is not the intent owner |
| 404 | resume_failed | Intent not found |
| 409 | resume_failed | Intent status is not paused |
| 500 | resume_failed | Store-level failure |
cURL
curl -s -X POST "$BASE/api/v1/intents/intent-1719763200000-a1b2c3d4/resume" \ -H "Authorization: Bearer $TOKEN" | jq .
DELETE /api/v1/intents/:id
Cancel an intent (soft-delete). Sets the status to cancelled with statusReason: "User cancelled". Only intents in active or paused status can be cancelled. This is a terminal operation — cancelled intents cannot be resumed or modified.
Response 200
{
"status": "cancelled",
"message": "intent intent-1719763200000-a1b2c3d4 cancelled"
}
Errors
| Status | error | When |
|---|---|---|
| 403 | cancel_failed | Caller is not the intent owner, or intent is in a non-cancellable state (completed, error) |
| 404 | cancel_failed | Intent not found |
| 500 | cancel_failed | Store-level failure |
cURL
curl -s -X DELETE "$BASE/api/v1/intents/intent-1719763200000-a1b2c3d4" \ -H "Authorization: Bearer $TOKEN" | jq .
GET /api/v1/strategies
List all available strategy templates. No authentication required. Returns hardcoded template metadata — for full default parameter values, see the Strategy Engine templates table.
Response 200
{
"strategies": [
{
"type": "treasury",
"label": "Treasury Management",
"description": "Conservative index-tracking with minimal drift. Low-cost, capital-preservation focus. Designed for DAO treasuries and institutional allocators.",
"icon": "shield"
},
{
"type": "hedge",
"label": "Portfolio Hedge",
"description": "Protect against downside risk. Lower strike = cheaper protection. Short-dated rolls for active portfolio managers.",
"icon": "umbrella"
},
{
"type": "leverage",
"label": "Leveraged Exposure",
"description": "Amplified index exposure with higher drift tolerance. High strike for maximum delta per unit capital. For sophisticated traders.",
"icon": "trending-up"
},
{
"type": "yield",
"label": "Yield Generation",
"description": "Passive income via options premium. Longest maturity for lowest roll frequency. Designed for depositors seeking steady returns.",
"icon": "piggy-bank"
},
{
"type": "custom",
"label": "Custom Strategy",
"description": "Full manual control. No defaults enforced — all parameters must be explicitly configured. For advanced users with specific requirements.",
"icon": "settings"
}
],
"fetchedAt": "2026-06-30T12:00:00Z"
}
cURL
# No auth required curl -s "$BASE/api/v1/strategies" | jq .
GET /api/v1/portfolio
Aggregated portfolio view for the authenticated user. Returns all intents with a summary section showing counts and averages. Auth is optional — without a token, returns an empty portfolio with owner: "".
Response 200
{
"owner": "alice",
"intents": [
{
"id": "intent-1719763200000-a1b2c3d4",
"type": "treasury",
"ticker": "BTC/USDC",
"status": "active",
"collateralAsset": "USDC",
"collateralAmount": "50000.00",
"strikeRatio": 0.50,
"currentDriftBps": 150,
"totalRolls": 3
},
{
"id": "intent-1719763300000-b2c3d4e5",
"type": "yield",
"ticker": "BTC/USDC",
"status": "paused",
"collateralAsset": "USDC",
"collateralAmount": "25000.00",
"strikeRatio": 0.50
}
],
"summary": {
"totalIntents": 3,
"activeIntents": 2,
"pausedIntents": 1,
"totalCapital": "0",
"avgStrikeRatio": 0.55
},
"fetchedAt": "2026-06-30T12:00:00Z"
}
currentDriftBps and totalRolls are
omitempty — they only appear when the intent has populated runtime state.
totalCapital is hardcoded to "0" in the current implementation
(requires an on-chain query to compute accurately — planned for a follow-up release).
Errors
| Status | error | When |
|---|---|---|
| 401 | unauthorized | Missing bearer token (if auth is required) |
| 500 | portfolio_failed | Store-level error listing intents |
cURL
# Authenticated portfolio curl -s "$BASE/api/v1/portfolio" \ -H "Authorization: Bearer $TOKEN" | jq . # Unauthenticated (returns empty portfolio) curl -s "$BASE/api/v1/portfolio" | jq .
Common patterns
Authentication
All mutating endpoints (POST, PUT, DELETE) and most read endpoints require a bearer token.
The token's sub claim maps to the Canton user ID, which is used as the intent
owner. Obtain tokens from Keycloak:
TOKEN="$(curl -s -X POST https://keycloak.int.medici.loan/realms/medici/protocol/openid-connect/token \ -d grant_type=password \ -d client_id=app-user-validator \ -d username="$USERNAME" \ -d password="$PASSWORD" \ -d client_secret="$CLIENT_SECRET" | jq -r '.access_token')"
Error response shape
All errors follow the same JSON structure:
{
"error": "error_code_string",
"message": "human-readable explanation"
}
Status transition diagram
┌──────────┐ pause ┌──────────┐
│ ACTIVE │ ──────────────> │ PAUSED │
│ │ <────────────── │ │
└────┬──┬──┘ resume └────┬─────┘
│ │ │
cancel │ complete cancel
│ │ │
v v v
┌──────────┐ ┌──────────┐
│CANCELLED │ │COMPLETED │ ── terminal (immutable)
└──────────┘ └──────────┘
┌──────────┐
│ ERROR │ ── terminal (immutable, agent-triggered)
└──────────┘
Id generation
Intent IDs use the format intent-{unixMilliseconds}-{8randomHexChars},
e.g. intent-1719763200000-a1b2c3d4. These are generated server-side
on creation and are guaranteed unique by the store.