Settlements & Exceptions

Follow transfers and allocations, work the issuance queue, and monitor rejections.

🚧

Beta

Canton 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

StageWhat it isEndpointTier
Transfer instructionA sender's request to move a holding. It waits for the receiver until expiresAt.GET /token/transfer-instructionstoken
AllocationHoldings committed to a named settlement, with an allocation deadline and a settlement deadline.GET /token/allocationstoken
Settled transferThe registry's proof that a position moved between two parties.GET /dtcc/transfersdtcc
RejectionAn operation the registry declined, with a free-text reason.GET /dtcc/rejectionsdtcc
Issuance request or offerA mint or burn that has been asked for or offered, and is awaiting a decision.GET /dtcc/issuance-requestsdtcc
Supply eventA mint or burn that was recorded as executed.GET /dtcc/supply-eventsdtcc

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", None
export 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:

  • allocationDeadlineAt is when every leg must be allocated. A settlement with any leg unallocated past this instant cannot proceed.
  • settlementDeadlineAt is 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:

operationWhat was declined
transferA transfer of a holding.
mint, burnAn issuance request or offer.
credential_offerA credential that an issuer offered to a party.
provider_service_requestA party's request to be onboarded as a provider.
registrar_service_requestA party's request to be onboarded as a registrar.
dvpA delivery-versus-payment settlement that did not settle after both sides agreed.
dvp_proposalA 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

reason 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:

directionMeaningWho decides
requestedA holder asked the registry to mint or burn.The registry.
offeredThe 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

SymptomWhere 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


Did this page help you?