Settlements & Exceptions
Follow transfers and allocations, work the issuance queue, and monitor rejections.
BetaCanton Data Services is in Beta. Endpoints and response fields can change as the product evolves.
The validator's active view holds only the present. A settled transfer leaves it, and a rejection reason is visible only as it happens. Canton Data Services exposes the records the ledger commits as REST resources, so an operations desk can answer three questions: what settled, what is waiting, and what was declined.
Settlement records at a glance
| Stage | What it is | Endpoint | Tier |
|---|---|---|---|
| Transfer instruction | A sender's request to move a holding. It waits for the receiver until expiresAt. | GET /token/transfer-instructions | token |
| Allocation | Holdings committed to a named settlement, with an allocation deadline and a settlement deadline. | GET /token/allocations | token |
| Settled transfer | The registry's proof that a position moved between two parties. | GET /dtcc/transfers | dtcc |
| Rejection | An operation the registry declined, with a free-text reason. | GET /dtcc/rejections | dtcc |
| Issuance request or offer | A mint or burn that has been asked for or offered, and is awaiting a decision. | GET /dtcc/issuance-requests | dtcc |
| Supply event | A mint or burn that was recorded as executed. | GET /dtcc/supply-events | dtcc |
Every record carries the contractId of its ledger record, which makes a stable key for de-duplication.
Find the outcome of a transfer instruction
A transfer instruction with status: open is awaiting the receiver's response. It can be accepted until expiresAt. After that it cannot settle, and the sender's locked holding is released.
status: closed says only that the instruction is no longer actionable. It does not say whether it was accepted, expired or rejected. To establish the outcome, look for a settled transfer with the same sender, receiver, instrument and amount.
def transfer_outcome(instruction):
"""Return ("settled", transfer) or ("not settled", None) for a closed instruction."""
transfers = pages(
"/dtcc/transfers",
senderPartyId=instruction["senderPartyId"],
receiverPartyId=instruction["receiverPartyId"],
instrumentId=instruction["instrumentId"],
**{"settledAt[gte]": instruction["createdAt"]},
)
for transfer in transfers:
linked = transfer.get("instructionContractId")
if transfer["amount"] == instruction["amount"] and linked in (None, instruction["contractId"]):
return "settled", transfer
return "not settled", Noneexport async function transferOutcome(instruction: any) {
const transfers = pages("/dtcc/transfers", {
senderPartyId: instruction.senderPartyId,
receiverPartyId: instruction.receiverPartyId,
instrumentId: instruction.instrumentId,
"settledAt[gte]": instruction.createdAt,
});
for await (const transfer of transfers) {
const linked = transfer.instructionContractId;
if (
transfer.amount === instruction.amount &&
(linked === undefined || linked === instruction.contractId)
) {
return { outcome: "settled", transfer };
}
}
return { outcome: "not settled", transfer: null };
}A settled transfer can carry instructionContractId, which points back at the instruction that authorised it. The field is optional, so the match above does not depend on it. Where it is present and matches, the link is exact. Treat it as an optimisation, not as the mechanism.
If no settled transfer matches, the instruction expired or was rejected. Query GET /dtcc/rejections with operation=transfer and the same parties to see whether the registry recorded a rejection. If it did not, the instruction most likely expired.
Settlement is final. A settled transfer is never reversed, and a correction appears as a separate compensating transfer rather than a change to the original record.
Follow a settlement and its deadlines
A delivery-versus-payment settlement is funded by allocations. Each side commits holdings to the settlement, and the settlement executes only once every leg is covered. settlementId is the key that joins the legs.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/allocations?settlementId=<SETTLEMENT_ID>"result = get(
"/token/allocations",
settlementId="<SETTLEMENT_ID>",
)
print(result)const result = await get("/token/allocations", {
settlementId: "<SETTLEMENT_ID>",
});
console.log(JSON.stringify(result, null, 2));The response shows each leg and the state it has reached: requested, instructed, allocated, executed, cancelled or withdrawn. A settlement with a leg still in requested or instructed is waiting on someone to allocate.
Two deadlines apply, and missing either fails the settlement:
allocationDeadlineAtis when every leg must be allocated. A settlement with any leg unallocated past this instant cannot proceed.settlementDeadlineAtis when execution must have happened.
Both come from the settlement's own terms, so every leg of one settlement carries the same pair.
To find allocations that are about to miss their window, combine state=requested with allocationDeadlineAt[lte]:
curl -s -g -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/token/allocations?state=requested&allocationDeadlineAt[lte]=2026-07-15T18:00:00.000Z"result = get(
"/token/allocations",
state="requested",
**{"allocationDeadlineAt[lte]": "2026-07-15T18:00:00.000Z"},
)
print(result)const result = await get("/token/allocations", {
state: "requested",
"allocationDeadlineAt[lte]": "2026-07-15T18:00:00.000Z",
});
console.log(JSON.stringify(result, null, 2));The -g flag stops curl from treating the square brackets as a range. In other clients, URL-encode the brackets.
Monitor rejections
GET /dtcc/rejections returns operations the registry declined. Each record carries the whole declined operation plus a reason.
Watch every operation, not only transfers
The registry declines more than transfers. An exception monitor that watches only transfers, mints and burns misses onboarding failures. The operation field says what was declined:
operation | What was declined |
|---|---|
transfer | A transfer of a holding. |
mint, burn | An issuance request or offer. |
credential_offer | A credential that an issuer offered to a party. |
provider_service_request | A party's request to be onboarded as a provider. |
registrar_service_request | A party's request to be onboarded as a registrar. |
dvp | A delivery-versus-payment settlement that did not settle after both sides agreed. |
dvp_proposal | A proposed settlement that the counterparty declined. |
The set is open. Read a value you do not recognise as "something else was declined" rather than failing.
The two settlement values differ. A declined dvp_proposal is an ordinary negotiation outcome, because the counterparty said no before anything was committed. A dvp rejection is the serious one. Both sides had already agreed terms, so something failed after commitment. It is the only rejection that records which party rejected it, in rejectedByPartyId.
Route on operation, show reason to a person
operation, show reason to a personreason is free text supplied by the rejecting party. The registry defines no code vocabulary for it, so there is nothing to branch on. Route on operation, and pass reason to an operator unchanged.
Poll for new rejections
from datetime import datetime, timedelta, timezone
seen = set()
def utc(moment):
return moment.isoformat(timespec="milliseconds").replace("+00:00", "Z")
def new_rejections(since):
"""Return rejections not seen before, from a window that overlaps the last poll."""
fresh = []
for rejection in pages("/dtcc/rejections", **{"rejectedAt[gte]": utc(since)}):
if rejection["contractId"] not in seen:
seen.add(rejection["contractId"])
fresh.append(rejection)
return fresh
# Poll every minute, looking back a few minutes so a late-arriving record is not missed.
fresh = new_rejections(datetime.now(timezone.utc) - timedelta(minutes=5))const seen = new Set<string>();
export async function newRejections(since: Date) {
const fresh: any[] = [];
for await (const rejection of pages("/dtcc/rejections", {
"rejectedAt[gte]": since.toISOString(),
})) {
if (!seen.has(rejection.contractId)) {
seen.add(rejection.contractId);
fresh.push(rejection);
}
}
return fresh;
}
// Poll every minute, looking back a few minutes so a late-arriving record is not missed.
const fresh = await newRejections(new Date(Date.now() - 5 * 60_000));Filtering on operation to an operation the registry never produces a rejection for returns an empty page, not an error.
Know what the feed cannot show
The rejections feed may be incomplete. The registry's reject choices write their rejection record optionally. An operation can be declined with no record appearing here. A declined operation is always observable by its absence: the pending record closes with no matching settlement or supply event. The explicit rejection, and therefore the reason, may not be captured.
Submissions that fail before the ledger commits anything leave no ledger record, so they cannot appear in any endpoint.
Work the issuance queue
GET /dtcc/issuance-requests returns mint and burn requests and offers. Use it for the queue an operations desk actually works. GET /dtcc/supply-events reports issuance that has already happened. This endpoint reports issuance that has been asked for.
direction is the field that matters. The registry models two opposite flows, and they put the obligation on different parties:
direction | Meaning | Who decides |
|---|---|---|
requested | A holder asked the registry to mint or burn. | The registry. |
offered | The registry offered a mint or burn to a holder. | The holder. |
To list exactly the items that await your action, filter on direction=offered and status=open. Add expiresAt[lte] to find what is about to lapse.
curl -s -g -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/dtcc/issuance-requests?direction=offered&status=open&partyId=<PARTY_ID>"result = get(
"/dtcc/issuance-requests",
direction="offered",
status="open",
partyId="<PARTY_ID>",
)
print(result)const result = await get("/dtcc/issuance-requests", {
direction: "offered",
status: "open",
partyId: "<PARTY_ID>",
});
console.log(JSON.stringify(result, null, 2));Mint and burn offers are served by this endpoint with operation and direction. For example, operation=mint&direction=offered returns mint offers.
A closed request does not say how it ended
Accept, reject and cancel are three different choices on the ledger, and all three leave the request closed. Finding no matching supply event and no rejection does not prove the request was cancelled, because the registry writes those records optionally.
When you must establish the outcome definitively, reconcile against the holder's positions. A mint that took effect always credits a holding. Read supply from positions and holdings, and treat GET /dtcc/supply-events as the audit trail of the issuance events that were recorded, not a guaranteed ledger of every issuance that occurred.
Where to look
| Symptom | Where to look |
|---|---|
| Funds look unavailable. | GET /token/allocations with state=allocated, then GET /token/holdings with isLocked=true. See Positions & Availability Checks. |
| A settlement is not progressing. | GET /token/allocations with its settlementId. Look for legs that are not yet allocated, and compare the deadlines with the current time. |
| A transfer instruction closed and no money moved. | GET /dtcc/transfers for a match, then GET /dtcc/rejections with operation=transfer. With neither, the instruction likely expired. |
| A mint or burn request closed. | GET /dtcc/supply-events, then GET /dtcc/rejections with operation=mint or burn. Confirm against positions. |
| A party's onboarding failed. | GET /dtcc/rejections with operation=provider_service_request, registrar_service_request or credential_offer. |
Next steps
Updated about 3 hours ago
