Idempotency and outcomes
Reconcile cached Platform API mutation responses with webhook-only on-chain outcomes, including final, retracted, and recalled events.
An idempotency key identifies one API instruction across retries. Webhook events describe observed activity and can revise an earlier event after a reorg or recall. A cached Platform API mutation response reflects the synchronous mutation outcome only. Use webhooks to observe that on-chain status.
Outcome model
Treat each signal according to the state it can prove.
| Signal | What it proves | What it does not prove |
|---|---|---|
| Cached Platform API mutation response | The request resolved to the instruction associated with this key. | The on-chain transaction is final forever. |
*.provisional webhook event | DALP observed the source log at inclusion. | The log has passed the configured reorg depth. |
*.final webhook event | The source block passed the configured reorg depth. | An unrelated later economic correction cannot occur. |
*.retracted webhook event | A reorg invalidated an earlier provisional observation. | The original cached Platform API response should remain your source of truth. |
*.recalled webhook event | An operator-authorized compliance recall superseded an earlier event. | That the earlier event reached your consumer before the recall became authoritative. Unknown supersedes references still need audit logging. |
This rule is the boundary between request idempotency and chain reconciliation. Treat an HTTP 201 returned from an idempotency cache as "the mutation was accepted and returned this response". Do not treat it as "the transaction is final forever".
Reconciliation pattern
Subscribe to the lifecycle events that can prove or revise an outcome:
*.finaland*.provisionalfor indexed outcomes, as documented for each event family.*.retractedfor events invalidated by a reorg.*.recalledfor operator-authorized compliance recall.
Dedupe delivery attempts by evt_id and logical outcomes by source_id. Correlate webhook events back to the originating API call with event.request.idempotency_key when that value is present. The value is the Idempotency-Key header you sent. If you put the key in double quotes, DALP removes the quotes, so "request-key" becomes request-key. If you do not send the header, DALP makes its own key for each transaction, and that value does not match a key that you sent. When one call submits a chain of transactions in order (for example, a yield schedule that is deployed and then attached to the token), the final transaction carries your key. Each earlier transaction in that chain carries its own key derived from your key, so a lookup by your key resolves to the final transaction. Other calls that send several separate writes put your key on each of those transactions, so several events can carry the same key. Examples are a role change that grants or revokes several roles, and a batch issuance that mints or burns in several parts. If a later retracted or recalled event names an earlier event through supersedes, treat the later event as authoritative over the cached Platform API response.
Webhook replay uses the same consumer rules. DALP can replay one event by evt_id, optionally guarded by chain ID, or replay a block range for a specific chain. A replay queues a fresh delivery envelope with its own evt_id and is_replay: true; it does not create a new on-chain outcome, change the original lifecycle state, or replace signature verification and delivery deduplication in your consumer.
A safe consumer stores at least these fields for reconciliation:
| Field | Source | Use |
|---|---|---|
webhook-id | Delivery header | Dedupe webhook delivery attempts. |
evt_id | Event body | Identify the delivered event envelope. |
source_id | Event body | Recognize the same logical source after an indexing retry. |
is_replay | Event body, when present | Mark replayed envelopes so downstream processing can suppress duplicate actions. |
lifecycle_state | Event body | Decide whether the event is final, retracted, recalled, pending, or provisional. |
request.idempotency_key | Event body, when present | Link the event to the original mutation request. |
supersedes | Event body for retracted and recalled states | Replace the earlier event with the later authoritative outcome. |
Retract and recall handling
When a *.retracted event arrives, mark the superseded provisional event as no longer valid and reverse any external side effects that assumed finality. A chain reorg never retracts a version 2 final event. When a *.recalled event arrives, apply the recall even if the original delivery failed or is unknown to your system. If the referenced event is unknown, retain the superseding event and log that the original is missing. Do not manufacture a reversal for an action your system never applied. If the original arrives later, prevent it from restoring the superseded state.
If both retraction and recall apply to the same original event, recall wins. Your consumer should keep the recall as the final superseding signal. Keep this retract and recall handling branch close to your idempotency correlation code. Cached Platform API responses must not mask later chain outcomes.
Replay handling
Use replay as a recovery tool, not as a shortcut around idempotent processing. DALP exposes webhook replay for a specific event ID or for a block range on a specific chain. A block-range replay needs the chain ID so the replay can be scoped to the right indexed chain.
Replay can deliver an event that your consumer has already seen. Keep the same safeguards in place during replay as during live delivery:
- Verify the webhook signature before processing the body.
- Dedupe each delivery attempt with the
webhook-idheader. - Reconcile the replayed body against
lifecycle_state,request.idempotency_key, andsupersedesbefore updating downstream state. - Store the stable domain identifiers from the event payload, such as chain, transaction, log, and resource identifiers documented on the event page. Use those identifiers to recognise an outcome that was already processed before replay.
- Treat replayed
retractedorrecalledevents as authoritative over the cached mutation response when they supersede an earlier outcome.
That pattern lets an operations team recover missed deliveries without double-applying a mint, transfer, settlement, or recall in the receiving system. The delivery envelope tells you whether you have seen that delivery attempt. The event payload tells you whether you have already processed the underlying on-chain outcome.
Typed SDK supersedes accessor
The typed SDK exposes supersedes only on retracted and recalled lifecycle states. That lets TypeScript consumers branch on lifecycle state before applying authoritative-over-cached-response behavior. Some retractions, such as trading-venue.trade-settled.retracted, mark the field optional: they omit it when your endpoint was never sent the earlier event. When the earlier delivery was sent but never succeeded, supersedes can name an event your consumer never stored. In both cases, match the retraction to your records by its payload identifiers, and log an unknown supersedes for audit.
The following integration skeleton is illustrative. Supply the raw body, verified headers, signing secret, and your own markSuperseded persistence function.
import { verifyWebhook } from "@settlemint/dalp-sdk/verify-webhook";
import type { Webhook } from "@settlemint/dalp-sdk";
const result = await verifyWebhook({ rawBody, headers, secret });
if (!result.ok) throw new Error(result.code);
const event: Webhook.Event = result.event;
if ((event.lifecycle_state === "retracted" || event.lifecycle_state === "recalled") && event.supersedes !== undefined) {
await markSuperseded({
originalEventId: event.supersedes,
supersedingEventId: event.evt_id,
idempotencyKey: event.request?.idempotency_key,
});
}verifyWebhook returns the discriminated event union after signature verification, so reconciliation code can narrow on event.type or event.lifecycle_state.
Consumer safeguards
Keep the reconciliation code conservative:
- Apply webhook signature verification before reading the event body.
- Dedupe delivery attempts with the
webhook-idheader, then process the event body exactly once for that delivery. - Treat an event with
is_replay: trueas a replayed envelope for an existing outcome, not as permission to apply the business action again. - Store cached mutation responses separately from event outcome state.
- Allow a later
retractedevent to supersede a provisional observation and arecalledevent to supersede a recallable source. - Treat missing
request.idempotency_keyas an event-only outcome. Do not invent a request correlation key.
Related references
- Read the events catalogue for the event types available to webhook consumers.
- Use token transfer final as a final lifecycle event example.
- Use token transfer retracted as a retracted lifecycle event example.