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

ProtocolPortDescription
REST (HTTP)8083External REST API (Axum)
gRPC9083Internal 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

MethodPathAuthDescription
GET/api/token/healthNoneHealth check with ledger status and collection counts
GET/healthzNoneLiveness probe
GET/metricsNonePrometheus metrics

Token Info

MethodPathAuthDescription
GET/api/token/supportedRequiredList supported token standards and capabilities
GET/api/token/infoRequiredService configuration and Canton ledger info
GET/api/token/statsRequiredAggregate statistics (collections, tokens, holdings)

Collections

MethodPathAuthDescription
POST/api/token/collectionsRequiredCreate a new token collection
GET/api/token/collectionsRequiredList collections for the tenant
GET/api/token/collections/:idRequiredGet collection details by ID

Minting

MethodPathAuthDescription
POST/api/token/mintRequiredMint new tokens within a collection
POST/api/token/mint/batchRequiredBatch mint tokens to multiple recipients in a single transaction

Transfers

MethodPathAuthDescription
POST/api/token/transferRequiredTransfer tokens between Canton parties
GET/api/token/transfersRequiredList transfer instructions (filters: party_id, status, direction)
GET/api/token/transfers/:idRequiredGet a transfer instruction by ID
POST/api/token/transfers/:id/actionRequiredAccept, reject, or cancel a transfer instruction

Holdings

MethodPathAuthDescription
GET/api/token/holdingsRequiredList holdings (filter by party_id, collection_id)
GET/api/token/holdings/:idRequiredGet a specific holding by ID
GET/api/token/balance/:partyIdRequiredGet aggregated token balance for a party

Burning

MethodPathAuthDescription
POST/api/token/burnRequiredBurn (destroy) tokens permanently

DvP Settlements (Delivery vs Payment)

MethodPathAuthDescription
POST/api/token/dvp/proposeRequiredPropose a DvP settlement between two parties
GET/api/token/dvpRequiredList DvP settlement proposals
POST/api/token/dvp/:id/acceptRequiredAccept a DvP proposal (counterparty confirms participation)
POST/api/token/dvp/:id/rejectRequiredReject a DvP proposal (unlocks all held assets)

Rewards

MethodPathAuthDescription
GET/api/token/rewardsRequiredGet rewards summary for the tenant
GET/api/token/rewards/activityRequiredGet rewards activity history

Protected Assets & Agreements

MethodPathAuthDescription
GET/api/token/assetsRequiredList TEE-protected assets
GET/api/token/assets/:idRequiredGet a protected asset by ID
GET/api/token/offersRequiredList agreement offers
GET/api/token/offers/:idRequiredGet an agreement offer by ID

Canton Integration

MethodPathAuthDescription
GET/api/canton/statusRequiredCanton ledger connection status
GET/api/canton/balanceRequiredCC/Amulet balance for a party
GET/api/canton/verify-ownershipRequiredVerify token ownership for a party
GET/api/canton/compliance/:token_idRequiredGet compliance status for a token
GET/api/canton/holdingsRequiredQuery Canton ledger holdings directly
GET/api/canton/contractsRequiredList active Canton contracts
POST/api/canton/exerciseRequiredExercise 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

FieldTypeRequiredDescription
namestringYesCollection display name
symbolstringYesShort ticker symbol (e.g., CC25)
descriptionstringNoHuman-readable description
standardstringYesFUNGIBLE, NON_FUNGIBLE, or SEMI_FUNGIBLE
mint_policystringNoOPEN, RESTRICTED, OWNER_ONLY, or CLOSED
transfer_restrictionstringNoUNRESTRICTED, SOUL_BOUND, TIME_LOCKED, WHITELIST_ONLY, or KYC_REQUIRED
max_supplyintegerNoMaximum token supply (omit for unlimited)
ownerstringYesCanton party ID of the collection owner
default_royaltyobjectNoDefault 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

ParameterTypeDescription
ownerstringFilter by owner party ID
standardstringFilter by token standard
organization_idstringFilter by organization
page_sizeintegerNumber of results per page (default 20)
page_tokenstringPagination 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

FieldTypeRequiredDescription
collection_idstringYesTarget collection ID
owner_party_idstringYesCanton party ID of the token recipient/owner after minting
amountstringNoAmount to mint (required for fungible tokens)
metadataobjectNoToken metadata (name, description, image, attributes)
royalty_configobjectNoPer-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

FieldTypeRequiredDescription
collection_idstringYesTarget collection ID for all mints in the batch
mintsarrayYesArray 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

FieldTypeRequiredDescription
from_party_idstringYesSender Canton party ID (must own the token)
to_party_idstringYesRecipient Canton party ID
token_idstringYesID of the token to transfer
amountstringNoAmount 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

ParameterTypeDescription
party_idstringFilter by sender or receiver party ID
statusstringFilter by status: PENDING, COMPLETED, REJECTED, CANCELLED, EXPIRED
directionstringFilter by direction relative to party_id: INCOMING or OUTGOING
page_sizeintegerNumber of results per page (default 20)
page_tokenstringPagination 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"
}
ActionWhoDescription
acceptReceiverAccept the instruction, complete the transfer atomically
rejectReceiverReject the instruction, unlock sender holdings
cancelSenderCancel 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

ParameterTypeDescription
party_idstringFilter by owner party ID
collection_idstringFilter by collection ID
include_lockedbooleanInclude holdings locked for pending transfers or DvP (default false)
page_sizeintegerNumber of results per page
page_tokenstringPagination 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

FieldTypeRequiredDescription
token_idstringYesID of the token to burn
owner_party_idstringYesCanton party ID of the token owner authorizing the burn
amountstringNoAmount 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

FieldTypeRequiredDescription
buyerstringYesParty receiving the delivery asset (pays with payment asset)
sellerstringYesParty receiving the payment asset (delivers the delivery asset)
delivery_collection_idstringYesCollection ID of the asset being delivered (seller to buyer)
delivery_amountstringYesAmount of delivery asset
payment_collection_idstringYesCollection ID of the payment asset (buyer to seller)
payment_amountstringYesAmount of payment asset
expires_atstringNoSettlement 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

StatusDescription
PROPOSEDSettlement proposed, initiator holdings locked pending counterparty response
ACCEPTEDCounterparty accepted; both parties' holdings locked, executing atomically
EXECUTEDAtomic swap completed successfully on the Canton ledger
REJECTEDCounterparty rejected; all holdings unlocked
EXPIREDSettlement passed its expiry without being accepted; all holdings unlocked
FAILEDExecution 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

StandardDescriptionUse Cases
FUNGIBLEInterchangeable units with decimal amountsCurrency, commodity credits, stablecoins
NON_FUNGIBLEUnique tokens with individual identityDigital certificates, ownership proofs, collectibles
SEMI_FUNGIBLEBatches of unique tokens with shared propertiesEvent tickets, fractional real estate, game items

Transfer Restrictions

ValueDescription
UNRESTRICTEDAny party may receive a transfer
SOUL_BOUNDNon-transferable; bound to the original recipient
TIME_LOCKEDTransfers restricted until a configured release time
WHITELIST_ONLYOnly parties on the collection whitelist may receive
KYC_REQUIREDRecipient must have completed KYC verification

Mint Policies

ValueDescription
OPENAny authenticated party may mint
RESTRICTEDOnly parties in the minters list may mint
OWNER_ONLYOnly the collection owner may mint
CLOSEDMinting 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

CodeDescription
COLLECTION_NOT_FOUNDCollection does not exist or belongs to a different tenant
TOKEN_NOT_FOUNDToken does not exist or is archived
HOLDING_NOT_FOUNDHolding ID does not exist
INSUFFICIENT_BALANCEParty does not hold enough tokens for the operation
MINT_POLICY_VIOLATIONCaller is not authorized to mint under the collection's mint policy
TRANSFER_RESTRICTEDTransfer blocked by collection restriction (KYC, whitelist, soul-bound)
TOKEN_FROZENToken is frozen and cannot be transferred or burned
HOLDING_LOCKEDHolding is locked by a pending transfer instruction or DvP
INSTRUCTION_NOT_FOUNDTransfer instruction ID does not exist
INSTRUCTION_EXPIREDTransfer instruction has passed its expiry time
DVP_NOT_FOUNDDvP settlement ID does not exist
DVP_ALREADY_SETTLEDDvP proposal has already been accepted, rejected, or executed
MAX_SUPPLY_EXCEEDEDMint would exceed the collection's maximum supply
UNAUTHORIZEDMissing or invalid X-API-Key / X-Tenant-Id
CANTON_UNAVAILABLECanton ledger connection is not available