A Canton integration is a webhook consumer first. An inbound offer is a status update on a transaction you never created, so there is nothing to poll for until the webhook fires.
Fireblocks delivers these to the endpoint you register. You implement the receiver. The envelope is the standard Fireblocks webhook envelope, not a Canton-specific one, and the data object is the standard transaction notification. The only Canton-specific thing on the wire is the cantonDetails key on data.
Which event fires
Only TRANSACTION_STATUS_UPDATED carries cantonDetails. A Canton integration can discard every other event type at the front door.
Route an incoming notification
To answer in step 6, call Answer an offer with data.id as offerId. Step 4 covers calls you submitted with Make a Canton call. Both are covered end to end in Write Canton Transactions.
Step 6 is the answerability rule. Branch on availableResponses, never on subStatus. Examples 1 and 4 below share status: CONFIRMING and subStatus: PENDING_RECEIVER_APPROVAL, and only one of them is answerable.
The five states to tell apart
1. Actionable offer
2. Response in flight
3. Allocation locked
4. Linked leg
5. Settlement receipt
An inbound DTCC end-investor onboarding offer. availableResponses carries both DTCC values, so this transaction ID is the one to answer. The same transaction as example 1, after a response was dispatched. availableResponses has emptied and approvalTransactionId names the outgoing response transaction. Posting again here returns 409 response-not-available.This is a genuine state, not a race. The offer is neither answerable nor resolved until the response transaction confirms. The outgoing offer-response transaction created by accepting an allocation, now locked on chain. This data.id is what ALLOCATION_WITHDRAW takes as allocationTransactionId, and this status pair is the only state in which it is accepted.The transaction stays here until the venue settles it, moving to COMPLETED, or a withdraw succeeds, moving it to CANCELLED. The vault-to-one-time-address transfer case, and the reason the answerability rule exists. One transfer offer flags two transactions with PENDING_RECEIVER_APPROVAL. This is the other one.It matches example 1 on every field you are tempted to filter on, including status, subStatus, and operation. It differs only in availableResponses being empty and originalTransactionId being set. Follow that pointer to find where the answer goes.This transaction is not broken, not a duplicate, and must not be ignored. It is half of the offer’s ledger footprint. It simply is not where the answer goes. The venue settled the allocation from example 3, and this is the credit side: a real Canton transaction, already COMPLETED when it is created, carrying the asset and amount you receive.There is nothing to answer and you did not initiate it, so it carries neither offerResponse nor call. Its settlement.originalTransactionId names the allocation it pays for. Without that pointer, this is an unexplained incoming credit.
Sub-status reference
Note that CONFIRMING is not transient for Canton. An accepted allocation sits at CONFIRMING with ALLOCATED until the venue settles it.
Delivery semantics
Delivery is at-least-once and unordered. Two notifications for the same transaction can arrive out of timestamp order, and timestamp is not a sequence number.
Treat every notification as a state snapshot rather than a transition. There is no previousStatus field, by design. Re-read availableResponses on each notification instead of tracking transitions yourself, and make handling idempotent on data.id plus data.status plus data.subStatus.
Parse permissively. Both the envelope and data carry fields beyond this contract, and a strict parser breaks on the next unrelated platform addition.
What your endpoint should return
Acknowledge first and process asynchronously. Holding the connection open while a handler runs risks a timeout, which is indistinguishable from a failure and triggers a redelivery.
Return 5XX for a transient failure, because redelivery is what you want. Do not return 4XX for a payload you will never accept, such as one with no cantonDetails, because that becomes a retry loop. Return 200 and drop it instead.
Reconciling without webhooks
The webhook is the push half of the read side. GET /v1/transactions/{txId} is the pull half, and both carry the identical top-level cantonDetails object. Use the pull path at startup, after downtime, or when a delivery fails. See Read Canton Transactions.