SettleMint
API referenceGuides

Post DALP webhook events to a ledger

The signed webhook feed that carries DALP activity, and the journal, account-key, reversal and idempotency rules an external ledger follows to post it.

Overview

DALP produces a signed feed of platform events. It does not post to a general ledger. The customer or fund administrator runs a subscriber that receives the feed, turns each event into a journal, and posts that journal to Xero or any other ledger. This page is the contract that subscriber needs: what is on the feed, how to trust it, how an event becomes a balanced journal, and how to keep the ledger correct when deliveries repeat, arrive late, or are undone.

The feed is the same webhook stream every integration receives. The accounting rules below sit on top of it; they add no second channel. For endpoint registration and the signature scheme in full, see Receive DALP webhooks.

Rendering diagram...

What is on the feed

An endpoint receives the events it subscribes to. Each delivery is one JSON envelope with these fields the ledger side reads:

FieldMeaning for a subscriber
evt_idIdentifies this delivery of the event. A replay of the same occurrence carries a new evt_id.
source_idIdentifies the underlying occurrence. It stays the same across replays. Not every event carries one.
type, versionThe event name and its schema version. Match rules on both.
lifecycle_stateOne of pending, provisional, final, retracted, recalled.
payloadThe typed event data, including amounts and timestamps.
supersedesOn a retraction, the evt_id of the event it replaces.
is_replaytrue on an envelope created by a replay request.
is_testtrue on a test event. Never post it.

The event catalog lists every event with its schema and an example. The feed carries these families today:

FamilyWhat it reports
tokenTransfers and corporate actions, including their retractions
assetAsset creation, issuance, burns, pause and unpause
primary-marketToken sale subscriptions, deliveries, refunds and proceeds withdrawals
trading-venueOrders, trades, trade settlement and their busts
xvpCross-party settlement lifecycle
complianceFreezes, unfreezes and verification verdicts
identityRegistration, claims and status
access-controlRole grants and revocations
custodyAccount creation and signer changes
fundingFunding requirements for operations

Fee events and custodial cash events are being added by separate work and are not on the feed yet. Until they ship, a ledger cannot post fees or custodial cash from the feed.

Not every event can become a journal. A journal needs an amount in the payload and an ISO 8601 timestamp for when it happened. For example, token.transfer.final carries an amount and a token address but no timestamp field, so a rule cannot post it as is. primary-market.subscription.final carries paymentAmount, paymentCurrencyAddress and occurredAt, so it can. Check an event's schema in the catalog before you write a rule for it.

Verify and accept a delivery

Treat the signature as the gate before any posting logic runs. DALP signs every delivery with the Standard Webhooks scheme and sends webhook-id, webhook-timestamp and webhook-signature, plus webhook-delivery-id and webhook-endpoint-id. Verify the signature against your stored signing secret with a Standard Webhooks library, and reject a timestamp outside a five-minute window. The signed content is the exact raw body, so verify before you parse it. While a secret rotates, webhook-signature can hold two signatures; either one is enough.

Answer with a 2xx as soon as you have stored the event durably, then post to the ledger afterwards. A reply slower than 20 seconds counts as a failure and triggers a retry of work you may already have done. The full reply table, secret rotation and counter-signed receipts are in Receive DALP webhooks and Webhook Delivery.

A signature proves DALP sent the message and that nobody altered it. It does not tell you the event is final. Read lifecycle_state next.

From event to journal

An organization keeps a rule table that maps events to journals. Two kinds of rule exist.

A posting rule names an event type and version, an optional exact-match condition on payload fields, a classification, a debit account key and a credit account key. It also names the payload fields that hold the amount, the asset, the timestamp, an optional stable domain key, and any reference fields to copy into the description. The classification is one of trades, fees, servicing or corrections.

A reversal rule names the event type it undoes and the payload field that identifies the original.

An account key is a stable, organization-chosen name such as investor-cash. The organization's account mapping resolves each key to a real ledger account, per classification, in Xero or elsewhere. Rules never carry ledger account codes, so a chart-of-accounts change touches the mapping and not the rules.

A rule table is only valid when all of these hold. Reject the whole table otherwise; do not fix it up silently.

  • Every account key resolves in the account mapping for the rule's classification.
  • No two rules can match the same event, and a type does not have both posting and reversal rules.
  • Every reversal rule targets a type that has a posting rule, and both key on the same field.
  • Every asset's currency is the ledger currency.
  • No posting rule targets a pending or provisional event.

Which events post

Lifecycle stateWhat the subscriber does
Test event (is_test)Skip.
pending, provisionalSkip. These can still be retracted, so they never post.
finalApply the matching posting rule.
retracted, recalledApply the matching reversal rule.

An event with no matching rule is refused with a named error rather than guessed at. Treat that refusal as a signal to extend the rule table, and keep the delivery for later.

The journal shape

A posting produces one balanced entry with two lines.

Entry fieldValue
sourceEntryIdThe envelope's source_id.
occurredAtThe rule's timestamp field.
narration<type> <source_id>.
Debit lineThe debit account key, the amount, and sourceLineId of <source_id>:debit.
Credit lineThe credit account key, the same amount, and <source_id>:credit.

Each line carries the classification, the ledger currency and a description made of the rule's text plus field=value pairs for the reference fields. Because both lines carry the same amount, the entry always balances.

Amounts

Payload amounts are integer strings in the asset's smallest unit. The rule's asset field names an entry in the organization's asset table, which gives the ledger currency and the asset's decimals. The subscriber converts exactly to cents with integer arithmetic. It never rounds. It refuses any amount that is zero, is not a whole number of cents, or is ten billion or more in the ledger currency. A refused amount needs a human decision: a sub-cent amount in the ledger currency is a rule or asset-table problem, not something to round away.

Worked example: one subscription

An investor subscribes to a token sale. DALP sends this event once the chain reaches finality for its block (fields not used below are omitted):

{
  "evt_id": "evt_7c12e838e3f9a8bafcaa292f86ed41e7",
  "source_id": "<source_id>",
  "type": "primary-market.subscription.final",
  "version": 1,
  "lifecycle_state": "final",
  "payload": {
    "saleAddress": "0x7777777777777777777777777777777777777777",
    "paymentCurrencyAddress": "0x8888888888888888888888888888888888888888",
    "paymentAmount": "1000000000",
    "occurredAt": "2026-10-12T09:15:00.000Z"
  }
}

The organization's rule table holds one posting rule for primary-market.subscription.final at version 1. It uses classification trades, debit key investor-cash, credit key subscription-liability, amount field paymentAmount, asset field paymentCurrencyAddress, timestamp field occurredAt and reference field saleAddress. Its asset table maps 0x8888…8888 to currency USD with 6 decimals.

The subscriber computes the amount: 1,000,000,000 base units at 6 decimals is 1000.00 USD, exactly 100,000 cents. It then builds this entry:

FieldValue
sourceEntryId<source_id>
occurredAt2026-10-12T09:15:00.000Z
narrationprimary-market.subscription.final <source_id>
Debitinvestor-cash, 1000.00 USD, line id <source_id>:debit
Creditsubscription-liability, 1000.00 USD, line id <source_id>:credit

The ledger connector then resolves investor-cash and subscription-liability through the trades account mapping and posts the entry. In Xero, that is one manual journal.

Reversals and idempotency

Two keys keep the ledger correct. Use each for its own job.

webhook-id (the evt_id) deduplicates deliveries. A retry repeats the same webhook-id, so record the ones you have handled and answer 2xx for any you recognise.

source_id makes the posting idempotent. A replay creates a new evt_id for the same occurrence, so evt_id alone would post it twice. Because the journal's sourceEntryId is the source_id, a replay produces the identical entry, and a ledger connector that keys on the organization plus sourceEntryId posts it once. An event with no source_id is refused, since a replay of it could not be detected.

Treat a posted entry as immutable. Never edit it to correct it. A correction is a new entry with a new source id.

The reversal key

Each posting also has a reversal key. It is <type>#<value> where the value is the payload field the rule names as its domain key, for example a trade id. When a posting rule declares no domain key, the key is <type>#source:<source_id>. Store the reversal key with the entry you post, because a reversal looks the original up by it.

A reversal event, such as a bust or a reorg retraction, names the original through its own domain-key field. The subscriber then:

  1. Builds the key <reversed type>#<value read from the retraction's payload>.
  2. Looks up every entry posted under that key for this organization.
  3. Posts one new entry whose lines mirror the originals: debit becomes credit and credit becomes debit, same account keys, same amounts, line ids <original line id>:reversal, and descriptions starting "Reversal of".
  4. Uses the retraction's own source_id as sourceEntryId, with narration <type> reverses <reversal key>.

If the lookup finds nothing, refuse the reversal and retry it later. The original may not have posted yet because deliveries can arrive out of order. Never drop the reversal and never post one for another organization's entry.

Delivery, retries, ordering and replay

DALP delivers at least once, never exactly once. A failed delivery retries on an exponential backoff from a 30-second base, doubling with random jitter, for up to three days from when the event was recorded. It retries 408, 429, 5xx, timeouts and connection failures. It stops at once on any other 4xx. Each attempt times out after 20 seconds. See Retries for the table.

Ordering is not guaranteed. The dispatcher picks work in chain order but sends deliveries concurrently, so the ledger side must be safe in any order. A retraction can arrive before the entry it undoes; the reversal refusal above is how a subscriber copes. A provisional event and its final event are separate deliveries.

Gaps are possible. For token movement, if DALP cannot write the notification, it still commits the movement and logs the failure, so a delivery can be missing without any signal on your side. Reconcile your ledger totals against DALP's reads on a schedule instead of assuming the feed is complete. The reasoning behind this is in Platform Event.

Replay fills a gap. POST /api/v2/webhooks/{id}/replays requeues events for an endpoint by block range (fromBlock, optional toBlock, chainId) or by one evtId. A replayed envelope has a new evt_id, is_replay: true and the original source_id, so replaying is safe for a subscriber that keys on source_id. After an outage longer than three days, replay is how you recover.

Frequently asked questions

Can I let DALP post to Xero for me?

No. DALP produces the signed feed and stops there. The posting step, the Xero credentials and the chart-of-accounts mapping stay with the customer or fund administrator.

Why not post provisional events and correct them later?

A provisional event can be retracted by a chain reorganisation. Posting it means posting a reversal later and showing both in the ledger. A final chain event is sent only after its block reaches the finality depth configured for the chain, so waiting for it avoids that churn. A final event can still be undone by a business reversal such as a trade bust, which is what the reversal rules cover.

What if my rules need a field the thin payload removes?

An endpoint's default thin payload shape strips personal data fields. Amounts and timestamps stay. If a rule reads a field that thin strips, the catalog lists it per event; change the endpoint to the full shape or choose a different field.

On this page