Positions & Availability Checks
Read computed positions, check how much is usable, and explain what is locked.
BetaCanton Data Services is in Beta. Endpoints and response fields can change as the product evolves.
Canton holds assets as individual holding contracts. There is no balance field to read, so answering "what do we have, and how much of it is usable?" normally means indexing every holding yourself. Canton Data Services does that for you and returns the result as a position: the sum of a party's open holdings of one instrument.
The endpoints in this guide are in the token tier, so they work for any asset that implements the Canton Token Standard, including DTCC registry securities and Canton Coin. They require the token tier to be licensed for your account.
Read a position
GET /token/positions returns one row per party and instrument.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/positions?partyId=<PARTY_ID>&instrumentId=<INSTRUMENT_ID>"result = get(
"/token/positions",
partyId="<PARTY_ID>",
instrumentId="<INSTRUMENT_ID>",
)
print(result)const result = await get("/token/positions", {
partyId: "<PARTY_ID>",
instrumentId: "<INSTRUMENT_ID>",
});
console.log(JSON.stringify(result, null, 2));{
"chainId": "canton:mainnet",
"partyId": "PARTY_A::1220a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"instrumentId": "912797RS8-912797RS8",
"instrumentAdminPartyId": "REGISTRY_REGISTRAR::1220e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1",
"total": { "amount": "200000020000", "amountFormatted": "20.0000020000" },
"locked": { "amount": "100000000000", "amountFormatted": "10.0000000000" },
"available": { "amount": "100000020000", "amountFormatted": "10.0000020000" },
"decimals": 10,
"holdingCount": 4,
"asOfOffset": "1491621"
}This is one element of the data array in the response. The key fields are:
| Field | Meaning |
|---|---|
total | Everything the party holds of this instrument, locked or not. |
locked | The quantity reserved against in-flight settlement. It counts toward the position but cannot fund anything else. |
available | The quantity that is unencumbered and able to fund a transfer. |
decimals | The scale shared by all three quantities. |
holdingCount | The number of open holding records that make up the position. |
asOfOffset | The ledger offset the position is as of. The same value is returned in the X-Ledger-Offset response header. |
The invariant total = locked + available always holds.
Each quantity is reported as amount, the raw unscaled integer as a string, and amountFormatted, the same value with the decimal point applied. In the example, decimals is 10, so the raw amount 100000020000 is 10.0000020000. Use amount for arithmetic. Quantities are strings because registry quantities can exceed the range a JSON number holds exactly.
instrumentAdminPartyId identifies the registry that administers the instrument. The token standard identifies an instrument by the pair of admin party and instrument identifier, so instrumentId alone is unique only within one issuer. Compare both when you hold assets from more than one registry.
Check availability
An availability check answers one question: can this party fund an amount of this instrument right now?
Check against available, never total. A position's total includes amounts that are already committed to other settlements, so a check against total oversells a position that is committed elsewhere.
from decimal import Decimal
def can_fund(position, required):
"""Return True if the position's available quantity covers `required`.
`required` is a decimal string such as "1.5", in the instrument's display units.
"""
required_raw = Decimal(required).scaleb(position["decimals"])
if required_raw != required_raw.to_integral_value():
raise ValueError("amount has more decimal places than the instrument supports")
return int(position["available"]["amount"]) >= int(required_raw)interface Position {
decimals: number;
available: { amount: string };
}
/** Returns true if the position's available quantity covers `required`, a decimal string such as "1.5". */
export function canFund(position: Position, required: string): boolean {
const [whole, fraction = ""] = required.split(".");
if (fraction.length > position.decimals) {
throw new Error("amount has more decimal places than the instrument supports");
}
const requiredRaw = BigInt(whole + fraction.padEnd(position.decimals, "0"));
return BigInt(position.available.amount) >= requiredRaw;
}The check compares raw integers, so no floating-point rounding is involved. Do not parse amountFormatted or convert quantities to floating-point numbers.
Before you act on the result:
- Check dataset freshness. Call
GET /native/healthand comparelatestEventAtwith the current time. Ifstatusisdegraded, the position can be stale. - Keep the offset. Record
asOfOffsetalongside your decision. Two responses that carry the same offset are directly comparable, and a higher offset is strictly newer. Quote it when you report a discrepancy.
To list only the positions that can fund something, add hasAvailable=true. This returns positions with a non-zero available quantity.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/positions?partyId=<PARTY_ID>&hasAvailable=true"result = get(
"/token/positions",
partyId="<PARTY_ID>",
hasAvailable="true",
)
print(result)const result = await get("/token/positions", {
partyId: "<PARTY_ID>",
hasAvailable: "true",
});
console.log(JSON.stringify(result, null, 2));Explain what is locked
When a position shows a locked quantity, three endpoints say why and for how long.
Allocations
An allocation sets aside a holding for a named settlement: the owner commits a specific amount of a specific instrument to a specific transfer leg. GET /token/allocations explains why each locked amount is locked and who it is committed to.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/allocations?partyId=<PARTY_ID>&instrumentId=<INSTRUMENT_ID>&state=allocated"result = get(
"/token/allocations",
partyId="<PARTY_ID>",
instrumentId="<INSTRUMENT_ID>",
state="allocated",
)
print(result)const result = await get("/token/allocations", {
partyId: "<PARTY_ID>",
instrumentId: "<INSTRUMENT_ID>",
state: "allocated",
});
console.log(JSON.stringify(result, null, 2));An allocation moves through these states:
state | Meaning | Holding locked |
|---|---|---|
requested | The settlement's coordinator has asked the owner to allocate. | No |
instructed | The owner has begun allocating, but it is not committed yet. | No |
allocated | Committed to the settlement. | Yes |
executed | The settlement ran and the transfer happened. | No, the holding moved. |
cancelled | Released before settling. | No |
withdrawn | Retracted by the party that raised it. | No |
Only allocated withholds funds. To reconcile locked on a position, filter to state=allocated.
Each allocation carries a settlementId, which is the join key between the two sides of a settlement. It also carries two deadlines, allocationDeadlineAt and settlementDeadlineAt. Every leg of one settlement carries the same pair, so the deadlines are useful for a countdown and no use for telling legs apart.
Locked holdings
GET /token/holdings returns the individual holding records. Filter on isLocked=true to see the locked ones.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/holdings?partyId=<PARTY_ID>&instrumentId=<INSTRUMENT_ID>&isLocked=true&status=open"result = get(
"/token/holdings",
partyId="<PARTY_ID>",
instrumentId="<INSTRUMENT_ID>",
isLocked="true",
status="open",
)
print(result)const result = await get("/token/holdings", {
partyId: "<PARTY_ID>",
instrumentId: "<INSTRUMENT_ID>",
isLocked: "true",
status: "open",
});
console.log(JSON.stringify(result, null, 2));A locked holding can include these fields:
lockedBylists the parties holding the lock, and therefore the parties able to release it.lockContextis the reason recorded by the locking party. It can be empty.lockExpiresAtis when the lock lapses on its own, without anyone acting. A holding with nolockExpiresAtstays locked until a locker releases it.
lockExpiresAt is what separates a settlement in progress from one that has stalled.
Transfer instructions
A holding can also be reserved against a transfer instruction that is waiting for the receiver. GET /token/transfer-instructions with status=open lists them. An open instruction can still be accepted until its expiresAt. After that the instruction cannot settle, and the sender's locked holding is released.
Spot a fragmented position
holdingCount tells you how many records make up a position. A high count against a small amount means the position is fragmented. This matters because a transfer must consume whole holdings as its inputs, so a fragmented position can be harder to move than its available figure suggests.
Reconcile a position against its holdings
A position is derived. The ledger has no position record, only the holdings that the API sums. For audit, you can rebuild a position yourself.
- List the party's holdings for one instrument with
status=open. Omitsort, because this endpoint returns holdings oldest first and accepts onlycreatedAt:asc. - Sum the raw
amountof every holding as integers. Include locked holdings, because a lock reserves a position rather than removing it. - Compare the sum with
totalon the position. Sum only the holdings whereisLockedistrueto compare withlocked.
Do this at a quiet moment or accept small differences. The holdings and the position are read at slightly different times, so a settlement that lands between the two reads shows up as a difference. Compare asOfOffset with GET /native/health to see whether the dataset moved between your reads.
A holding with status: closed was superseded by a transfer or burn. The ledger never edits a holding. Every change closes the prior record and opens a new one, and closed records stay queryable for reconciliation.
Next steps
Updated about 3 hours ago
