# Idempotency and on-chain outcome

Source: https://docs.settlemint.com/docs/events/idempotency-and-on-chain-outcome
Reconcile cached DAPI mutation responses with webhook-only on-chain outcomes, including final, retracted, and recalled events.



DAPI idempotency protects the synchronous API mutation path. Webhook lifecycle events describe the later on-chain outcome. Keep those two signals separate. A cached DAPI mutation response reflects the synchronous mutation outcome only: the platform accepted the mutation and returned the same response for the same idempotency key. It cannot prove that the originating transaction stayed final on-chain. Use webhooks to observe that on-chain status.

## Outcome model [#outcome-model]

Treat each signal according to the state it can prove.

| Signal                        | What it proves                                                                        | What it does not prove                                                                                                                         |
| ----------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Cached DAPI mutation response | The mutation request was accepted and returned this response for the idempotency key. | The on-chain transaction is final forever.                                                                                                     |
| `*.final` webhook event       | The event passed the configured reorg depth.                                          | A later recall cannot supersede it.                                                                                                            |
| `*.retracted` webhook event   | A reorg invalidated an earlier event.                                                 | The original cached DAPI 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 [#reconciliation-pattern]

Subscribe to the lifecycle events that can prove or revise an outcome:

* `*.final` for events that passed the configured reorg depth.
* `*.retracted` for events invalidated by a reorg.
* `*.recalled` for operator-authorized compliance recall.

Dedupe every delivery by the `webhook-id` header. Correlate webhook events back to the originating API call with `event.request.idempotency_key` when that value is present. If a later retracted or recalled event names an earlier event through `supersedes`, treat the later event as authoritative over the cached DAPI 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.                                           |
| `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 [#retract-and-recall-handling]

When a `*.retracted` event arrives, mark the superseded event as no longer valid and reverse any external side effects that assumed finality. When a `*.recalled` event arrives, apply the recall even if the original delivery failed or is unknown to your system. Unknown `supersedes` references should be ignored after audit logging.

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](#retract-and-recall-handling) branch close to your idempotency correlation code. Cached DAPI responses must not mask later chain outcomes.

## Replay handling [#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-id` header.
* Reconcile the replayed body against `lifecycle_state`, `request.idempotency_key`, and `supersedes` before 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 `retracted` or `recalled` events 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 [#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.

```typescript
import { verifyWebhook, type Webhook } from "@settlemint/dalp-sdk";

const result = 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") {
  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 [#consumer-safeguards]

Keep the reconciliation code conservative:

* Apply webhook signature verification before reading the event body.
* Dedupe delivery attempts with the `webhook-id` header, then process the event body exactly once for that delivery.
* Treat an event with `is_replay: true` as 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 later `retracted` and `recalled` events to supersede earlier accepted or final events.
* Treat missing `request.idempotency_key` as an event-only outcome. Do not invent a request correlation key.

## Related references [#related-references]

* Read the [events catalogue](/docs/events) for the event types available to webhook consumers.
* Use [token transfer final](/docs/events/token-transfer-final) as a final lifecycle event example.
* Use [settlement transfer retracted](/docs/events/settlement-transfer-retracted) as a retracted lifecycle event example.
