Overview
Webhooks send DALP platform events as signed POST requests to an HTTPS endpoint, an address your integration registers to receive them, so your system learns of each event without polling the Platform API. The routes on this page register an endpoint, verify its signatures, send test events, rotate the signing secret, and update, switch off or delete the endpoint. The webhooks reference covers which events the platform sends and how to consume them. The Console offers the same endpoint controls under Webhooks.
DALP uses Standard Webhooks headers and signing. Verify the signature before you trust a delivery. Deliveries can repeat, and the consumer contract defines the identifiers to store and deduplicate.
Every route on this page needs a caller with webhook management permission, which the Owner role holds, and an active organisation. The authentication guide describes the accepted credentials.
Register an endpoint
POST /api/ creates an endpoint. The URL must be a public HTTPS address unless the deployment operator has configured an exception for private-network receivers. The platform refuses private or loopback destinations without that exception. The webhook management API defines the complete request and response.
curl -X POST "https://your-instance.example.com/api/v2/webhooks" \
-H "X-Api-Key: sm_dalp_..." \
-H "Idempotency-Key: webhook-create-001" \
-H "Content-Type: application/json" \
-d '{
"url": "https://webhooks.example.com/dalp",
"displayName": "Settlement notifications"
}'The platform answers 200 with the endpoint:
{
"data": {
"id": "0199c4e2-5b7a-7d10-9a3e-2f61c8b04d17",
"tenantId": "org_7Hq2kWm9",
"systemId": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"url": "https://webhooks.example.com/dalp",
"displayName": "Settlement notifications",
"subscriptions": ["*.provisional", "*.final", "*.retracted"],
"defaultPayloadShape": "thin",
"counterSignedReceipts": false,
"receiptTimeoutSeconds": 30,
"breakerState": "closed",
"disabledAt": null,
"disabledReason": null,
"fatEventsAcknowledgment": null,
"secret": {
"activeVersion": 1,
"previousVersion": null,
"previousRevokesAt": null,
"lastUsedAt": null
},
"createdAt": "2026-11-09T08:14:05.000Z",
"updatedAt": "2026-11-09T08:14:05.000Z",
"version": 1,
"createdByUserId": "usr_4Fp8nQx2",
"signingSecret": "dalp_whsk_sample_not_a_live_secret"
},
"links": {
"self": "/api/v2/webhooks/0199c4e2-5b7a-7d10-9a3e-2f61c8b04d17"
}
}data.id is the {webhookId} that every later route on this page takes. The first response includes signingSecret. Store it immediately, because a later endpoint read never returns it. Retrying the same registration body with the same Idempotency-Key returns the existing endpoint with signing, not a second endpoint. A changed body under that key returns 409 WEBHOOK_. A repeat while registration is still running returns 409 WEBHOOK_. If the original response was lost, read the endpoint and rotate its secret to obtain a new one.
A System is an organisation's on-chain DALP deployment. Without a deployed System the platform answers 404 WEBHOOK_. An organisation can hold at most 1,000 endpoints across all its networks, and a further registration answers 409 WEBHOOK_. An endpoint that is switched off still counts toward that limit, and a deleted endpoint does not. A new endpoint receives only events that happen after its creation: for chain events, the blocks from the chain head at that moment onwards. Earlier history is never sent to it.
When subscriptions is omitted, the platform applies the default subscriptions. Those patterns match event name suffixes, not lifecycle states. Check the events the defaults skip before choosing the full subscription list for your integration.
To track an on-chain write from broadcast to its outcome, subscribe to transaction.request.*. The default subscriptions skip this family, because its names, such as transaction., do not end in .final. The transaction request catalogue describes its five events.
Every new endpoint starts with the thin payload shape, which removes personal data fields. To receive those fields, register the endpoint first, then update it to fat with the required acknowledgement. The payload shape contract defines that acknowledgement.
The event envelope
Every delivery contains an event envelope with evt_id, type, version, lifecycle_state, related resource references and a typed payload, and every delivery except a test event carries source_id. A retraction adds supersedes, a Replay sets is_replay, and a test event sets is_test. request.idempotency_key appears when the platform can attribute the transaction to one caller key. The consumer contract defines each identifier. The machine-readable event definitions and TypeScript SDK define the payloads. The event catalogue explains when each event is sent and what your receiver does with it.
A token.transfer.final delivery for one leg of a venue trade carries this body:
{
"evt_id": "evt_a4eac9d5432394cd91b380838ddbd4a5",
"source_id": "chain:537001:0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:7:0:token.transfer.final@2",
"type": "token.transfer.final",
"version": 2,
"lifecycle_state": "final",
"payload": {
"tokenAddress": "0x5555555555555555555555555555555555555555",
"chainId": 537001,
"from": "0x6666666666666666666666666666666666666666",
"to": "0x7777777777777777777777777777777777777777",
"amount": "250000000000000000000",
"transactionHash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"blockNumber": 18445240,
"logIndex": 7,
"occurredAt": "2026-09-28T09:18:00.000Z",
"venueTrade": {
"tradeId": "0x2222222222222222222222222222222222222222222222222222222222222222",
"leg": "base",
"marketId": "0x1111111111111111111111111111111111111111111111111111111111111111",
"venueAddress": "0x9999999999999999999999999999999999999999"
}
},
"related": {
"idxr_token": "0x5555555555555555555555555555555555555555"
}
}To follow bank payments, subscribe to bank-rail.credit.* or bank-rail.cash-out.*. The defaults skip these milestone names. Events report committed statuses with decimal bank amounts, currency and occurredAt. Approval and loss-accounting details stay private. See the bank rail catalogue for the credit, recall and payout return rules.
Verify the signature
The platform sends three Standard Webhooks headers and two DALP headers:
Verify the exact raw body against your stored signing secret before you parse or trust the payload. The verifier is exported as verifyWebhook from @settlemint/. It accepts timestamps within five minutes of your clock by default, and its timestamp option changes that tolerance. The platform re-signs a stored envelope before an attempt when its signature is at least four minutes old, so a delivery that waited in backoff still arrives inside the five-minute window. Keep your receiver's clock synchronised.
A receiver verifies the request, stores the event, and answers 2xx. Here secrets holds the active signing secret, and the previous one during a rotation, and storeEvent stands for your own durable store:
import { verifyWebhook } from "@settlemint/dalp-sdk/verify-webhook";
export async function receive(request: Request, secrets: ReadonlyArray<string>): Promise<Response> {
const result = await verifyWebhook({
rawBody: await request.text(),
headers: request.headers,
secret: secrets,
});
if (!result.ok) {
return new Response(result.code, { status: 401 });
}
await storeEvent(result.event);
return new Response(null, { status: 204 });
}On success verifyWebhook returns { ok: true, event, release }. On failure it returns { ok: false, code }, where code is TIMESTAMP_SKEW, SECRET_MISMATCH, BODY_HASH_MISMATCH or MALFORMED_BODY. With a replay store, code can also be REPLAYED_EVENT or REPLAY_STORE_FAILURE. The verifier strips the dalp_whsk_ prefix from each secret itself. A 4xx answer other than 408 or 429 ends delivery of the event, so a receiver whose clock drifts, or that drops the previous secret too early, loses events until you recover them with Resend or Replay.
Refuse a repeated delivery
The platform can send one event more than once. A retry and a Resend keep the webhook-id of the first attempt. With the optional replayStore, the verifier accepts each webhook-id once and refuses the copies that follow.
A store has two methods. The verifier makes a new claim id for each request and gives it to both methods. claim(event records the event id with the claim id. It returns true only when no record for that event id exists. release(event removes the record only while the record still holds that claim id. The verifier adds no locking of its own. Your claim must check and record the id in one atomic write, such as a Redis SET with NX that stores the claim id. A read followed by a separate write lets two copies that arrive together both pass. Your release must compare the claim id and delete the record in one atomic step, such as a short Lua script in Redis. Keep each record for at least the three-day retry window. A record that expires sooner lets a later retry run your handler again. A Resend also keeps the webhook-id, and an operator can send one until the platform deletes the delivery history, 30 days after delivery ended. When a Resend must not run your handler again, keep each record for that long.
The verifier checks the timestamp, the signature and the JSON body first. A request that fails one of these checks never reaches the store, so a request without a valid signature never answers REPLAYED_EVENT. The verifier then claims the event id:
- When the id is already claimed, the verifier returns
REPLAYED_EVENT. Answer with a2xx, so the platform stops sending that copy. When your endpoint requires counter-signed receipts, submit a receipt for that delivery too. - When
claimthrows, the verifier tries to release its claim id, ignores an error from that call, and returnsREPLAY_STORE_FAILURE. Answer with a5xx, so the platform retries. When that release works, a claim that the store recorded before it threw is removed, and the retry can verify. When that release also fails, the claim stays.
When your handler fails, call release on the verified result before you answer 5xx. It removes the claim of this request only. The platform retries with the same webhook-id, and the retry verifies again. Call release only when you answer with a status that the platform retries. When you answer 2xx, keep the claim, so a later copy does not run your handler again. Without a replay store, release does nothing.
import { verifyWebhook, type WebhookReplayStore } from "@settlemint/dalp-sdk/verify-webhook";
export async function receive(
request: Request,
secrets: ReadonlyArray<string>,
replayStore: WebhookReplayStore
): Promise<Response> {
const result = await verifyWebhook({
rawBody: await request.text(),
headers: request.headers,
secret: secrets,
replayStore,
});
if (!result.ok) {
if (result.code === "REPLAYED_EVENT") {
return new Response(null, { status: 204 });
}
return new Response(result.code, { status: result.code === "REPLAY_STORE_FAILURE" ? 503 : 401 });
}
try {
await storeEvent(result.event);
} catch {
await result.release();
return new Response(null, { status: 503 });
}
return new Response(null, { status: 204 });
}A claim holds while your handler runs. The platform ends an attempt that gets no answer within 20 seconds and retries it later. A retry that arrives while your handler still runs returns REPLAYED_EVENT, and your 2xx acknowledges it even when the handler then fails. A claim also stays when your process stops before it calls release, when release throws, or when the verifier cannot release its claim after claim throws. The next copy of that event is then acknowledged without being processed. Log the webhook-id before you answer 5xx for REPLAY_STORE_FAILURE or after a release that throws.
To recover such an event, delete its id from your store and then Resend the delivery. A Resend keeps the webhook-id. A Replay also recovers the event, because it sends the event under a new webhook-id. When you cannot accept these cases, do not use a replay store. Record the webhook-id in the same transaction as your own writes, and acknowledge an id that is already there.
Send a test event
POST /api/ queues a test event for one endpoint, whatever its subscriptions. The optional body field eventType names the event, and it defaults to webhook.test.final. The envelope carries is_ and the payload { "test": true }, so your receiver can check its signature verification without acting on a real event.
A test event also reaches an endpoint that is switched off. It never counts toward the circuit breaker, which switches an endpoint off after 30 failed attempts in a row. Each API server accepts at most 10 test events a minute for an endpoint and at most 100 for an organisation, because each server counts its own requests. A request over either limit answers 429 WEBHOOK_, the code the replay limit also uses.
Rotate the signing secret
POST /api/ returns a new signingSecret and keeps the previous secret valid for 24 hours. During that overlap, the platform signs deliveries with both secrets, and webhook-signature holds both signatures. Store the new secret immediately, then accept either secret until previousRevokesAt passes.
Rotation does not replay its original response for the same Idempotency-Key. Another rotation creates another secret and revokes any older previous secret immediately. After an uncertain rotation response, read the endpoint with GET /api/ before deciding whether to rotate again. Its secret object returns activeVersion, previousVersion and previousRevokesAt, never secret material.
DELETE /api/ ends the overlap early. The platform refuses it until a delivery signed with the new secret has succeeded, so send a test event first.
Update an endpoint
PATCH /api/ changes url, displayName, subscriptions, defaultPayloadShape, fatEventsAcknowledgment, counterSignedReceipts, receiptTimeoutSeconds, disabled and disabledReason. Every field is optional, and the platform leaves an omitted field untouched. Supplying subscriptions replaces the full stored list. The subscriptions reference defines valid patterns.
A switch to fat, or a subscription change on a fat endpoint, requires fatEventsAcknowledgment to cover every personal data field the resulting subscriptions imply. Payload shape changes affect new events only. The payload shape contract covers the fields and stored payloads.
Changing url retargets pending deliveries. The platform refuses the change while deliveries are pending unless you send ?acknowledgePending=true. A subscription-only change does not require that acknowledgement.
To keep an update from overwriting a change saved after you read the endpoint, send the endpoint's version as expectedVersion. Every endpoint response carries it as the top-level version, which starts at 1 and rises by one with each update that sets at least one field, even to the value already stored. It is not secret.activeVersion, which numbers the signing secret. Rotating the signing secret and the circuit breaker switching the endpoint off or back on leave version unchanged, so a matching version does not prove the endpoint is still on or off: read breakerState and disabledAt again before you send disabled. Without expectedVersion the platform applies the update to whatever is stored, so the last update wins.
When expectedVersion differs from the stored version, the platform refuses the update with 409 WEBHOOK_ (DALP-0542) and changes nothing. The response does not include the stored version, and sending the same request again fails the same way. Read the endpoint, apply your change to what it returns, and send its version as expectedVersion. The Console's “Edit endpoint” sheet always sends expectedVersion.
Switch off or delete an endpoint
Setting disabled to true cancels the endpoint's queued deliveries and needs ?acknowledgePending=true while deliveries are pending. Events that happen while the endpoint is off are not queued for it, and neither Resend nor Replay recovers them later. Setting disabled to false enables the endpoint again and resets its circuit breaker.
DELETE /api/ switches the endpoint off permanently and cancels its queued deliveries, including scheduled retries. Its delivery history stays readable until the retention cleanup removes it.
Next steps
- Retries defines response classes, backoff, and the retry window.
- Resend and replay covers recovery after an endpoint failure.
- Counter-signed receipts defines the optional signed acknowledgement.
- Chain of custody reads the recorded dispatch history of one event.
- Errors covers the error envelope a failed webhook management call returns.