Token Service API
Canton-native digital assets service for creating and managing tokenized assets on the Canton Network. Supports fungible tokens, NFTs, and semi-fungible tokens with a CIP-56 compliant UTXO-style holding model, atomic DvP settlement, royalties, and asset agreement licensing. Implemented in Rust (Axum + Tonic).
Service Architecture
| Protocol | Port | Description |
|---|
| REST (HTTP) | 8083 | External REST API (Axum) |
| gRPC | 9083 | Internal gRPC API (Tonic/Prost) |
Authentication
All protected endpoints require both headers:
X-API-Key: Your API key in the format tnz_{tenant}_{secret}X-Tenant-Id: Your tenant UUID
Public endpoints (/api/token/health, /healthz, /metrics) do not require authentication.
Endpoint Reference
Health
| Method | Path | Auth | Description |
|---|
GET | /api/token/health | None | Health check with ledger status and collection counts |
GET | /healthz | None | Liveness probe |
GET | /metrics | None | Prometheus metrics |
Token Info
| Method | Path | Auth | Description |
|---|
GET | /api/token/supported | Required | List supported token standards and capabilities |
GET | /api/token/info | Required | Service configuration and Canton ledger info |
GET | /api/token/stats | Required | Aggregate statistics (collections, tokens, holdings) |
Collections
| Method | Path | Auth | Description |
|---|
POST | /api/token/collections | Required | Create a new token collection |
GET | /api/token/collections | Required | List collections for the tenant |
GET | /api/token/collections/:id | Required | Get collection details by ID |
Minting
| Method | Path | Auth | Description |
|---|
POST | /api/token/mint | Required | Mint new tokens within a collection |
POST | /api/token/mint/batch | Required | Batch mint tokens to multiple recipients in a single transaction |
Transfers
| Method | Path | Auth | Description |
|---|
POST | /api/token/transfer | Required | Transfer tokens between Canton parties |
GET | /api/token/transfers | Required | List transfer instructions (filters: party_id, status, direction) |
GET | /api/token/transfers/:id | Required | Get a transfer instruction by ID |
POST | /api/token/transfers/:id/action | Required | Accept, reject, or cancel a transfer instruction |
Holdings
| Method | Path | Auth | Description |
|---|
GET | /api/token/holdings | Required | List holdings (filter by party_id, collection_id) |
GET | /api/token/holdings/:id | Required | Get a specific holding by ID |
GET | /api/token/balance/:partyId | Required | Get aggregated token balance for a party |
Burning
| Method | Path | Auth | Description |
|---|
POST | /api/token/burn | Required | Burn (destroy) tokens permanently |
DvP Settlements (Delivery vs Payment)
| Method | Path | Auth | Description |
|---|
POST | /api/token/dvp/propose | Required | Propose a DvP settlement between two parties |
GET | /api/token/dvp | Required | List DvP settlement proposals |
POST | /api/token/dvp/:id/accept | Required | Accept a DvP proposal (counterparty confirms participation) |
POST | /api/token/dvp/:id/reject | Required | Reject a DvP proposal (unlocks all held assets) |
Rewards
| Method | Path | Auth | Description |
|---|
GET | /api/token/rewards | Required | Get rewards summary for the tenant |
GET | /api/token/rewards/activity | Required | Get rewards activity history |
Protected Assets & Agreements
| Method | Path | Auth | Description |
|---|
GET | /api/token/assets | Required | List TEE-protected assets |
GET | /api/token/assets/:id | Required | Get a protected asset by ID |
GET | /api/token/offers | Required | List agreement offers |
GET | /api/token/offers/:id | Required | Get an agreement offer by ID |
Canton Integration
| Method | Path | Auth | Description |
|---|
GET | /api/canton/status | Required | Canton ledger connection status |
GET | /api/canton/balance | Required | CC/Amulet balance for a party |
GET | /api/canton/verify-ownership | Required | Verify token ownership for a party |
GET | /api/canton/compliance/:token_id | Required | Get compliance status for a token |
GET | /api/canton/holdings | Required | Query Canton ledger holdings directly |
GET | /api/canton/contracts | Required | List active Canton contracts |
POST | /api/canton/exercise | Required | Exercise a choice on a Canton contract |
Health Check
GET /api/token/health
Response
{
"healthy": true,
"version": "1.0.0",
"ledger_status": "connected",
"active_collections": 12,
"total_tokens": 5430
}
Create Collection
POST /api/token/collections
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"name": "Carbon Credits 2025",
"symbol": "CC25",
"description": "Verified carbon offset credits for 2025",
"standard": "FUNGIBLE",
"mint_policy": "RESTRICTED",
"transfer_restriction": "KYC_REQUIRED",
"max_supply": 1000000,
"owner": "party::issuer::abc123",
"default_royalty": {
"percentage": 250,
"recipient": "party::treasury::xyz789",
"enforced": true
}
}
Request Parameters
| Field | Type | Required | Description |
|---|
name | string | Yes | Collection display name |
symbol | string | Yes | Short ticker symbol (e.g., CC25) |
description | string | No | Human-readable description |
standard | string | Yes | FUNGIBLE, NON_FUNGIBLE, or SEMI_FUNGIBLE |
mint_policy | string | No | OPEN, RESTRICTED, OWNER_ONLY, or CLOSED |
transfer_restriction | string | No | UNRESTRICTED, SOUL_BOUND, TIME_LOCKED, WHITELIST_ONLY, or KYC_REQUIRED |
max_supply | integer | No | Maximum token supply (omit for unlimited) |
owner | string | Yes | Canton party ID of the collection owner |
default_royalty | object | No | Default royalty config applied to all tokens in this collection |
Response
{
"collection": {
"id": "col_a1b2c3d4",
"name": "Carbon Credits 2025",
"symbol": "CC25",
"description": "Verified carbon offset credits for 2025",
"standard": "FUNGIBLE",
"mint_policy": "RESTRICTED",
"transfer_restriction": "KYC_REQUIRED",
"max_supply": 1000000,
"total_supply": 0,
"owner": "party::issuer::abc123",
"minters": [],
"contract_id": "00abcdef...",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
},
"transaction_id": "tx_11223344"
}
List Collections
GET /api/token/collections?owner=party::issuer::abc123&standard=FUNGIBLE&page_size=20
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Query Parameters
| Parameter | Type | Description |
|---|
owner | string | Filter by owner party ID |
standard | string | Filter by token standard |
organization_id | string | Filter by organization |
page_size | integer | Number of results per page (default 20) |
page_token | string | Pagination cursor from previous response |
Response
{
"collections": [
{
"id": "col_a1b2c3d4",
"name": "Carbon Credits 2025",
"symbol": "CC25",
"standard": "FUNGIBLE",
"total_supply": 50000,
"owner": "party::issuer::abc123",
"created_at": "2025-01-15T10:30:00Z"
}
],
"next_page_token": "eyJsYXN0X2lkIjoiY29sXzQ0In0=",
"total_count": 12
}
Get Collection Details
GET /api/token/collections/col_a1b2c3d4
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Response
{
"id": "col_a1b2c3d4",
"name": "Carbon Credits 2025",
"symbol": "CC25",
"description": "Verified carbon offset credits for 2025",
"standard": "FUNGIBLE",
"mint_policy": "RESTRICTED",
"transfer_restriction": "KYC_REQUIRED",
"max_supply": 1000000,
"total_supply": 50000,
"owner": "party::issuer::abc123",
"minters": ["party::issuer::abc123"],
"contract_id": "00abcdef...",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-16T08:00:00Z"
}
Mint Tokens
POST /api/token/mint
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"collection_id": "col_a1b2c3d4",
"owner_party_id": "party::buyer::def456",
"amount": "1000",
"metadata": {
"name": "Carbon Credit Batch #42",
"description": "Verified Verra VCS credits",
"attributes": [
{ "trait_type": "vintage_year", "value": "2025" },
{ "trait_type": "project_type", "value": "reforestation" },
{ "trait_type": "country", "value": "Brazil" }
]
}
}
Request Parameters
| Field | Type | Required | Description |
|---|
collection_id | string | Yes | Target collection ID |
owner_party_id | string | Yes | Canton party ID of the token recipient/owner after minting |
amount | string | No | Amount to mint (required for fungible tokens) |
metadata | object | No | Token metadata (name, description, image, attributes) |
royalty_config | object | No | Per-token royalty override (overrides collection default) |
Response
{
"token": {
"id": "tok_99aabbcc",
"collection_id": "col_a1b2c3d4",
"owner": "party::buyer::def456",
"amount": "1000",
"status": "ACTIVE",
"contract_id": "00112233...",
"created_at": "2025-01-15T11:00:00Z"
},
"transaction_id": "tx_55667788"
}
Batch Mint Tokens
Mint tokens to multiple recipients in a single atomic transaction. All mints succeed or all fail together.
POST /api/token/mint/batch
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"collection_id": "col_a1b2c3d4",
"mints": [
{
"owner_party_id": "party::buyer::def456",
"amount": "500",
"metadata": { "name": "Batch Item 1" }
},
{
"owner_party_id": "party::investor::mno345",
"amount": "500",
"metadata": { "name": "Batch Item 2" }
}
]
}
Request Parameters
| Field | Type | Required | Description |
|---|
collection_id | string | Yes | Target collection ID for all mints in the batch |
mints | array | Yes | Array of mint objects, each with owner_party_id, amount, and optional metadata |
Response
{
"tokens": [
{
"id": "tok_aa11bb22",
"collection_id": "col_a1b2c3d4",
"owner": "party::buyer::def456",
"amount": "500",
"status": "ACTIVE"
},
{
"id": "tok_cc33dd44",
"collection_id": "col_a1b2c3d4",
"owner": "party::investor::mno345",
"amount": "500",
"status": "ACTIVE"
}
],
"transaction_id": "tx_batchaabb",
"total_minted": "1000"
}
Transfer Tokens
POST /api/token/transfer
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"from_party_id": "party::buyer::def456",
"to_party_id": "party::market::ghi789",
"token_id": "tok_99aabbcc",
"amount": "500"
}
Request Parameters
| Field | Type | Required | Description |
|---|
from_party_id | string | Yes | Sender Canton party ID (must own the token) |
to_party_id | string | Yes | Recipient Canton party ID |
token_id | string | Yes | ID of the token to transfer |
amount | string | No | Amount to transfer (for fungible/semi-fungible tokens) |
Response
{
"success": true,
"transaction_id": "tx_aabbccdd",
"token": {
"id": "tok_99aabbcc",
"owner": "party::market::ghi789",
"amount": "500",
"status": "ACTIVE"
},
"royalty_paid": {
"token_id": "tok_99aabbcc",
"amount": "12.50",
"currency": "CC",
"recipients": [
{ "party": "party::treasury::xyz789", "amount": "12.50" }
]
}
}
List Transfers
GET /api/token/transfers?party_id=party::buyer::def456&status=PENDING&direction=OUTGOING
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Query Parameters
| Parameter | Type | Description |
|---|
party_id | string | Filter by sender or receiver party ID |
status | string | Filter by status: PENDING, COMPLETED, REJECTED, CANCELLED, EXPIRED |
direction | string | Filter by direction relative to party_id: INCOMING or OUTGOING |
page_size | integer | Number of results per page (default 20) |
page_token | string | Pagination cursor from previous response |
Transfer Instruction Actions
POST /api/token/transfers/instr_aabb1122/action
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"action": "accept",
"party": "party::market::ghi789"
}
| Action | Who | Description |
|---|
accept | Receiver | Accept the instruction, complete the transfer atomically |
reject | Receiver | Reject the instruction, unlock sender holdings |
cancel | Sender | Cancel before acceptance, unlock sender holdings |
Holdings (CIP-56 UTXO Model)
Holdings represent individual UTXO-style ownership contracts on the Canton ledger (CIP-56). A party may hold multiple holdings of the same token type.
GET /api/token/holdings?collection_id=col_a1b2c3d4&party_id=party::buyer::def456
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Query Parameters
| Parameter | Type | Description |
|---|
party_id | string | Filter by owner party ID |
collection_id | string | Filter by collection ID |
include_locked | boolean | Include holdings locked for pending transfers or DvP (default false) |
page_size | integer | Number of results per page |
page_token | string | Pagination cursor |
Response
{
"holdings": [
{
"id": "hold_11223344",
"collection_id": "col_a1b2c3d4",
"owner": "party::buyer::def456",
"amount": "400",
"lock_holder": "",
"created_at": "2025-01-15T11:00:00Z",
"updated_at": "2025-01-15T14:00:00Z"
}
],
"next_page_token": "",
"total_count": 1,
"total_amount": "400"
}
Get Token Balance
Returns the aggregated token balance across all holdings for a given party, optionally filtered by collection.
GET /api/token/balance/party::buyer::def456?collection_id=col_a1b2c3d4
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Response
{
"party_id": "party::buyer::def456",
"balances": [
{
"collection_id": "col_a1b2c3d4",
"collection_name": "Carbon Credits 2025",
"symbol": "CC25",
"total_amount": "400",
"available_amount": "200",
"locked_amount": "200"
}
],
"total_collections": 1
}
Burn Tokens
POST /api/token/burn
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"token_id": "tok_99aabbcc",
"owner_party_id": "party::buyer::def456",
"amount": "100"
}
Request Parameters
| Field | Type | Required | Description |
|---|
token_id | string | Yes | ID of the token to burn |
owner_party_id | string | Yes | Canton party ID of the token owner authorizing the burn |
amount | string | No | Amount to burn (for fungible tokens; omit to burn entire holding) |
Response
{
"success": true,
"transaction_id": "tx_eeff0011",
"burned_amount": "100",
"remaining_amount": "300"
}
DvP Settlement
Delivery vs Payment (DvP) enables atomic token-for-token swaps on the Canton ledger. Both legs settle simultaneously — if either fails, neither executes. The proposer initiates the settlement; the counterparty then accepts or rejects.
Propose DvP Settlement
POST /api/token/dvp/propose
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"buyer": "party::buyer::def456",
"seller": "party::seller::jkl012",
"delivery_collection_id": "col_a1b2c3d4",
"delivery_amount": "100",
"payment_collection_id": "col_stablecoin_usdc",
"payment_amount": "5000",
"expires_at": "2025-01-15T18:00:00Z"
}
Request Parameters
| Field | Type | Required | Description |
|---|
buyer | string | Yes | Party receiving the delivery asset (pays with payment asset) |
seller | string | Yes | Party receiving the payment asset (delivers the delivery asset) |
delivery_collection_id | string | Yes | Collection ID of the asset being delivered (seller to buyer) |
delivery_amount | string | Yes | Amount of delivery asset |
payment_collection_id | string | Yes | Collection ID of the payment asset (buyer to seller) |
payment_amount | string | Yes | Amount of payment asset |
expires_at | string | No | Settlement expiry timestamp (ISO 8601) |
Response
{
"settlement": {
"id": "dvp_99887766",
"buyer": "party::buyer::def456",
"seller": "party::seller::jkl012",
"delivery_collection_id": "col_a1b2c3d4",
"delivery_amount": "100",
"payment_collection_id": "col_stablecoin_usdc",
"payment_amount": "5000",
"status": "PROPOSED",
"created_at": "2025-01-15T16:00:00Z",
"expires_at": "2025-01-15T18:00:00Z"
},
"locked_delivery_holdings": [
{ "id": "hold_seller001", "amount": "100", "lock_holder": "party::seller::jkl012" }
],
"locked_payment_holdings": [
{ "id": "hold_buyer001", "amount": "5000", "lock_holder": "party::buyer::def456" }
]
}
Accept DvP
POST /api/token/dvp/dvp_99887766/accept
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"party": "party::seller::jkl012"
}
Reject DvP
POST /api/token/dvp/dvp_99887766/reject
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"party": "party::seller::jkl012",
"reason": "Price no longer acceptable"
}
List DvP Proposals
GET /api/token/dvp?party_id=party::buyer::def456&status=PROPOSED
X-API-Key: tnz_acme_s3cr3t
X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000
DvP Lifecycle
| Status | Description |
|---|
PROPOSED | Settlement proposed, initiator holdings locked pending counterparty response |
ACCEPTED | Counterparty accepted; both parties' holdings locked, executing atomically |
EXECUTED | Atomic swap completed successfully on the Canton ledger |
REJECTED | Counterparty rejected; all holdings unlocked |
EXPIRED | Settlement passed its expiry without being accepted; all holdings unlocked |
FAILED | Execution failed on the ledger; all holdings unlocked |
Canton Network Concepts
The Token Service builds directly on Canton Network primitives. Key concepts:
- Parties — Identities on the Canton ledger, referenced as
party::name::id - Collections — On-ledger asset class contracts governing minting, transfer rules, and royalties
- Holdings (CIP-56) — UTXO-style ownership contracts; each holding represents a discrete amount owned by a party
- Transfers — Two-step transfer protocol where sender locks holdings and receiver accepts or rejects, enabling atomic multi-party workflows
- DvP — Atomic delivery-vs-payment swaps using Canton's deterministic finality; a propose/accept model where rejection or expiry unlocks all assets
Token Standards
| Standard | Description | Use Cases |
|---|
FUNGIBLE | Interchangeable units with decimal amounts | Currency, commodity credits, stablecoins |
NON_FUNGIBLE | Unique tokens with individual identity | Digital certificates, ownership proofs, collectibles |
SEMI_FUNGIBLE | Batches of unique tokens with shared properties | Event tickets, fractional real estate, game items |
Transfer Restrictions
| Value | Description |
|---|
UNRESTRICTED | Any party may receive a transfer |
SOUL_BOUND | Non-transferable; bound to the original recipient |
TIME_LOCKED | Transfers restricted until a configured release time |
WHITELIST_ONLY | Only parties on the collection whitelist may receive |
KYC_REQUIRED | Recipient must have completed KYC verification |
Mint Policies
| Value | Description |
|---|
OPEN | Any authenticated party may mint |
RESTRICTED | Only parties in the minters list may mint |
OWNER_ONLY | Only the collection owner may mint |
CLOSED | Minting is permanently disabled |
Royalty Configuration
Royalties are configured in basis points (100 = 1%) and can be split across multiple recipients. They are automatically calculated and distributed on each token transfer.
{
"default_royalty": {
"percentage": 500,
"recipient": "party::creator::abc",
"enforced": true,
"splits": [
{ "recipient": "party::creator::abc", "percentage": 80 },
{ "recipient": "party::platform::xyz", "percentage": 20 }
]
}
}
gRPC Service
The Token Service exposes the full TokenService proto on port 9083 for internal platform use. The gRPC API provides additional operations not available via REST:
- Merge and split holdings (
MergeHoldings, SplitHolding) - Lock and unlock individual holdings (
LockHolding, UnlockHolding) - Royalty distribution (
ConfigureRoyalty, DistributeRoyalty, GetRoyaltyStats) - Organization and namespace management
- Asset agreement registration and TEE execution (
RegisterAsset, ExecuteAgreement) - Freeze and unfreeze tokens (
FreezeToken, UnfreezeToken)
See proto/token.proto for the full proto definition.
Error Codes
| Code | Description |
|---|
COLLECTION_NOT_FOUND | Collection does not exist or belongs to a different tenant |
TOKEN_NOT_FOUND | Token does not exist or is archived |
HOLDING_NOT_FOUND | Holding ID does not exist |
INSUFFICIENT_BALANCE | Party does not hold enough tokens for the operation |
MINT_POLICY_VIOLATION | Caller is not authorized to mint under the collection's mint policy |
TRANSFER_RESTRICTED | Transfer blocked by collection restriction (KYC, whitelist, soul-bound) |
TOKEN_FROZEN | Token is frozen and cannot be transferred or burned |
HOLDING_LOCKED | Holding is locked by a pending transfer instruction or DvP |
INSTRUCTION_NOT_FOUND | Transfer instruction ID does not exist |
INSTRUCTION_EXPIRED | Transfer instruction has passed its expiry time |
DVP_NOT_FOUND | DvP settlement ID does not exist |
DVP_ALREADY_SETTLED | DvP proposal has already been accepted, rejected, or executed |
MAX_SUPPLY_EXCEEDED | Mint would exceed the collection's maximum supply |
UNAUTHORIZED | Missing or invalid X-API-Key / X-Tenant-Id |
CANTON_UNAVAILABLE | Canton ledger connection is not available |