Webhook endpoints
Configure DALP webhook endpoints, delivery privacy, idempotent mutations, signed receiver handling, retries, replay, and chain-of-custody proofs.
Overview
Webhook endpoints deliver selected DALP events to an external HTTPS URL. Use them when an integration needs pushed event delivery instead of polling token or account collections.
DALP also exposes inbound callback routes when an external custody or compliance service must report decisions back to the platform. These inbound callbacks are not webhook subscriptions. DALP owns the fixed intake routes and processes each callback before any customer-facing event delivery happens.
Use the webhook events reference and the AsyncAPI manifest to check event names, lifecycle states, and payload schemas before choosing endpoint subscriptions.
Endpoint model
Create endpoints with POST /api/v2/webhooks. The request includes:
| Field | Behaviour |
|---|---|
url | Required HTTPS target URL for delivery. |
displayName | Optional label, up to 200 characters. |
subscriptions | Event patterns to deliver. Defaults to *.final, *.retracted, and *.recalled. |
defaultPayloadShape | Must be thin when creating an endpoint. Switch to fat later with a PATCH request and the required field acknowledgement. |
counterSignedReceipts | Optional flag for endpoints that return signed delivery receipts. |
DALP accepts only public HTTPS receiver URLs. When you create an endpoint or change its URL, DALP resolves the hostname. The request is rejected if the URL does not use https://, the name cannot be resolved, or any resolved address falls in a blocked range: private, loopback, link-local, carrier-grade NAT, documentation, or IPv4 multicast. Development and test deployments can explicitly allow loopback URLs for local integration tests. Production endpoint URLs must resolve to public addresses.
The same check runs before delivery. If DNS later changes an accepted hostname to a blocked private or non-public address range, DALP will not dispatch the delivery to that address. Keep webhook receiver DNS stable, public, and under the operating team's control.
The create and rotate-secret responses reveal the signing secret once. Later reads return endpoint metadata and secret status, not the cleartext value. If a client retries the same create or rotate request with the same idempotency key, DALP returns the same response envelope with signingSecret: null so the cleartext value is not exposed again. Store the first dalp_whsk_... value securely, and do not overwrite it with null from an idempotent retry.
Mutation idempotency
Webhook mutations accept the Idempotency-Key header. Send a stable key when creating an endpoint, updating its settings, rotating secrets, retrying deliveries, replaying events, recalling events, or changing secret state from an automated workflow. If the network drops after DALP accepts the request, resend with the same key instead of submitting a second mutation.
Reuse an idempotency key only for the same request body. Reusing the key for a different body is rejected so one key cannot accidentally describe two different operations.
Keep API mutation idempotency separate from event consumption. The Idempotency-Key protects the request you send to DALP. Your webhook receiver still has to verify the delivered event and dedupe side effects.
Receiver verification and deduplication
Every delivered webhook should be handled as a signed, at-least-once message. Verify the raw request body with the endpoint signing secret before parsing the payload. Use verifyWebhook from the DALP SDK when you want the same verify-first pattern used by the event reference examples.
After verification, dedupe each delivery by the webhook-id header before applying side effects. For replayed or retried payloads, also dedupe the business effect by stable domain fields that survive across deliveries, such as chain ID, transaction hash, log index, resource ID, and lifecycle state when the event type includes them. A replay is another delivery of the same business event, not a new operation.
During signing-secret rotation, keep both the active and previous secrets available to your receiver until the 24-hour overlap ends. DALP may still deliver already-enqueued work signed with the previous secret during that window.
Payload privacy
DALP delivers thin payloads by default. Thin payloads omit configured personal-data fields for event types such as identity registration, access-control role changes, asset issuance, compliance freeze recalls, and token transfers.
Create the endpoint as thin first. Switching to fat requires a later PATCH /api/v2/webhooks/{id} request with a fatEventsAcknowledgment.fieldsAcknowledged list that covers every additional field implied by the subscriptions. DALP rejects the update when the acknowledgement does not match the subscription set.
Delivery operations
The routes below cover the full endpoint lifecycle: creation, delivery, retry, replay, recall, and audit.
| Operation | API route |
|---|---|
| List endpoints | GET /api/v2/webhooks |
| Read endpoint metadata | GET /api/v2/webhooks/{id} |
| Update URL, subscriptions, payload shape, receipt mode, or disabled state | PATCH /api/v2/webhooks/{id} |
| Disable an endpoint | DELETE /api/v2/webhooks/{id} |
| Enqueue a test event | POST /api/v2/webhooks/{id}/test-events |
| List delivery attempts | GET /api/v2/webhooks/{id}/deliveries |
| Read one delivery attempt | GET /api/v2/webhooks/{id}/deliveries/{deliveryId} |
| Retry one delivery event | POST /api/v2/webhooks/{id}/deliveries/{deliveryId}/retries |
| Replay historical events | POST /api/v2/webhooks/{id}/replays |
| Recall an event | POST /api/v2/webhooks/events/{evtId}/recall |
| Get chain-of-custody proof | GET /api/v2/webhooks/events/{evtId}/chain-of-custody |
| Rotate the signing secret | POST /api/v2/webhooks/{id}/rotate-secret |
| Revoke the previous signing secret | POST /api/v2/webhooks/{id}/revoke-previous-secret |
| Read delivery statistics | GET /api/v2/webhooks/stats |
When updating an endpoint URL while deliveries are pending, pass acknowledgePending=true only when you intend DALP to retarget those queued attempts to the new URL.
Secret rotation keeps the previous signing secret valid for a 24-hour overlap. Revoke the previous secret after DALP has observed delivery under the new secret.
Retry, replay, and consumer idempotency
Use the retry and replay routes for different recovery jobs:
| Recovery job | Use it when | Request shape | Consumer expectation |
|---|---|---|---|
| Retry one delivery | One recorded delivery attempt needs to be scheduled again for the same event. | POST /api/v2/webhooks/{id}/deliveries/{deliveryId}/retries | Dedupe by the webhook-id header before applying side effects. |
| Replay an event | A known event must be enqueued for the endpoint again. | POST /api/v2/webhooks/{id}/replays with evtId and optional chainId | Treat the replay as another delivery of the same event, not as a new business event. |
| Replay a block range | A receiver was offline or a new endpoint must catch up from historical chain events. | POST /api/v2/webhooks/{id}/replays with fromBlock, optional toBlock, chainId, and confirmLargeRange for large ranges | Dedupe delivery attempts by webhook-id. Dedupe business side effects by stable payload fields such as transaction hash, log index, and lifecycle state when the event type includes them. |
Replay responses return a replayId, the endpointId, and eventsEnqueued. The response also includes snapshotToBlock when DALP bounded the replay to a chain snapshot. Replays enqueue delivery work; they do not change the original event's lifecycle state.
For request-to-chain reconciliation, keep Platform API idempotency and webhook result handling separate. A cached mutation response proves that DALP accepted the synchronous request for that idempotency key. The webhook event proves the later chain result. See idempotency and on-chain outcome for the consumer-side reconciliation pattern.
Delivery failure classes and retries
Each recorded attempt carries a failureClass when it did not succeed. List attempts with GET /api/v2/webhooks/{id}/deliveries or read one with GET /api/v2/webhooks/{id}/deliveries/{deliveryId} to see the value for a failed attempt. The same failure classes back the delivery and failure views in the Console.
DALP only re-attempts failures that a later attempt can plausibly recover from. A failure that would return the same result on every retry is terminal: DALP stops re-attempting it and records the failed attempt. Use the class to decide whether to wait for an automatic retry or to fix the receiver and trigger a manual retry. Automatic retries stop once an event is more than three days old, even when the failure class is retryable. After three days, trigger a manual retry or replay rather than waiting.
| Failure class | Meaning | Retried automatically |
|---|---|---|
HTTP_4XX | Deterministic client error from the receiver, such as 400, 401, 403, 404, or 422. | No (terminal) |
HTTP_4XX_RETRYABLE | A 408 Request Timeout or 429 Too Many Requests response. | Yes |
HTTP_5XX | Server error response from the receiver, such as 500, 502, or 503. | Yes |
DNS_FAIL | The receiver hostname could not be resolved. | Yes |
TLS_FAIL | The TLS or certificate handshake with the receiver failed. | Yes |
CONNECT_TIMEOUT | The connection to the receiver timed out or was aborted. | Yes |
READ_TIMEOUT | The receiver accepted the connection but did not return a response body in time. | Yes |
INVALID_RESPONSE | The receiver returned a response DALP could not classify as success. | Yes |
RECEIPT_TIMEOUT | A counter-signed receipt was not submitted before the receipt window closed. | Yes |
RECEIPT_INVALID_SIG | A submitted counter-signed receipt failed signature verification. | No (terminal) |
RECEIPT_HASH_MISMATCH | A submitted counter-signed receipt did not match the delivered event hash. | No (terminal) |
A terminal HTTP_4XX means the request reached your receiver and was rejected for a reason that will not change on retry, such as authentication, authorization, a missing route, or a body your handler considers invalid. Fix the receiver, then trigger a manual retry with POST /api/v2/webhooks/{id}/deliveries/{deliveryId}/retries. Return 408 or 429 instead when your receiver is temporarily unable to accept the payload and you want DALP to keep retrying on its own.
Event recall
Use event recall when a previously recorded compliance freeze event should no longer be treated as current in the webhook timeline. Recall is available only for event types with a registered .recalled variant in the event manifest. DALP records the recalled event, the original it supersedes, and the recall reason.
curl --request POST "$DALP_API_URL/api/v2/webhooks/events/evt_01h.../recall" \
--header "X-Api-Key: $DALP_API_TOKEN" \
--header "Idempotency-Key: recall-evt-01h-20260608" \
--header "Content-Type: application/json" \
--data '{"reason":"Corrected compliance event after review"}'The path takes one parameter (evtId, required): the id of the webhook event to mark as recalled. The request body takes one field (reason, required): a human-readable recall reason from 1 to 2000 characters.
The response returns the original event id as evtId, the new superseding event id as recalledEvtId, the event it supersedes as supersedes, the submitted reason, and recalledByUserId for the API caller who triggered the operation. Delivered webhook events for a recall identify the supersession with supersedes. Fat deliveries also include the reason in reasonCode. Default thin deliveries omit that field, so use the API response if you need the submitted reason or caller attribution. Send an Idempotency-Key when the operation is driven by automation so a retry cannot create a second recall for the same request.
The caller must have the compliance recall permission for the active organisation. Without that permission, DALP returns the same not-found style error used for unavailable webhook events.
Ripple Custody inbound callbacks
POST /api/webhooks/ripple is the route Ripple Custody calls to report intent events. The provider calls this route when a custody intent changes state. DALP parses the envelope and acknowledges intermediate events. Terminal approval or rejection events are dispatched to the custody approval monitor for the matching intent.
This route is separate from outbound webhook endpoints created with POST /api/v2/webhooks. The table below shows each direction, its purpose, and the party that configures the URL.
| Direction | Purpose | Who configures the URL | Public API surface |
|---|---|---|---|
| Outbound webhook endpoint | DALP sends selected platform events to your HTTPS receiver | You create and manage the receiver URL | /api/v2/webhooks and child routes |
| Ripple Custody callback | Ripple Custody sends intent state changes back to DALP | The platform exposes the DALP callback URL to the provider integration | POST /api/webhooks/ripple |
Accepted provider envelope
DALP reads the Ripple Custody event type from payload.type, not from a top-level type field. The envelope can include metadata such as domainId, event id, savedAt, and sequenceNumber.
{
"domainId": "25aaec0d-e8dc-44b6-8070-9231f1ddadf0",
"id": "03424a3f-bdfc-4521-b8b2-933a8ff13cce",
"payload": {
"id": "35e4d8d9-f943-484b-865c-c736679ba0cc",
"type": "IntentApproved"
},
"savedAt": "2026-05-19T07:32:00.000Z",
"sequenceNumber": 369
}Terminal approval events resolve the custody approval as approved. Terminal rejection, failure, expiry, closed, or denied events resolve it as rejected. Intermediate events are acknowledged without changing the monitor state. Malformed or unrecognised events are also accepted without a monitor update.
Authentication and request limits
The Ripple callback body is capped at 64 KB before DALP verifies a signature or parses JSON. Requests over the cap return 413.
If RIPPLE_WEBHOOK_SECRET is set, DALP verifies an HMAC-SHA256 signature from x-ripple-webhook-signature, with x-webhook-signature as a fallback header. A signature mismatch returns 410 so the provider stops retrying a permanently unauthenticated delivery.
In production, DALP refuses to process Ripple callbacks when RIPPLE_WEBHOOK_SECRET is not set unless the deployment explicitly opts out with RIPPLE_INSECURE_NO_HMAC=1. Non-production deployments can proceed without the HMAC secret, but the integration then relies on HTTPS and network allowlisting at the deployment boundary.
Replay and retry behaviour
When the provider envelope includes both domainId and sequenceNumber, DALP records the highest sequence number seen for each (domainId, intent) pair. A duplicate or stale sequence is accepted with 200 and is not dispatched again. If the deduplication write fails, DALP still dispatches the terminal event: the approval monitor is idempotent for repeated resolution signals.
DALP returns one of the following provider-facing status codes.
| Condition | Response | Effect |
|---|---|---|
| Valid terminal event dispatched to the approval monitor | 200 | Delivery accepted |
| Intermediate, malformed, unrecognised, duplicate, or stale event | 200 | Delivery acknowledged without a monitor update |
| Body exceeds 64 KB | 413 | Delivery rejected as too large |
| Signature mismatch when the HMAC secret is configured | 410 | Delivery rejected permanently |
| Approval monitor is not reachable yet, or a retryable dispatch error occurs | 503 | Provider can retry delivery |
| Approval monitor rejects the signal as a permanent client error | 410 | Provider can stop retrying delivery |
Related pages: Compliance providers, Operational integration patterns, Webhook events.
Fireblocks Custody inbound callbacks
POST /api/webhooks/fireblocks is the route Fireblocks Custody calls to report transaction status events. When a vault transaction changes state, Fireblocks calls this route and DALP reads the terminal decision to resolve the matching custody approval. A pending transaction clears as soon as Fireblocks pushes the result instead of waiting on the next status poll.
DALP keeps polling Fireblocks as a backstop, so an approval still resolves even if a webhook is missed. You do not call this route. The platform exposes the callback URL to the Fireblocks workspace, and Fireblocks delivers status updates to it.
Like the Ripple Custody callback, this route is separate from outbound webhook endpoints at POST /api/v2/webhooks. The table below shows each direction, its purpose, and the party that configures the URL.
| Direction | Purpose | Who configures the URL | Public API surface |
|---|---|---|---|
| Outbound webhook endpoint | DALP sends selected platform events to your HTTPS receiver | You create and manage the receiver URL | /api/v2/webhooks and child routes |
| Fireblocks Custody callback | Fireblocks sends transaction status changes back to DALP | The platform exposes the DALP callback URL to the Fireblocks workspace | POST /api/webhooks/fireblocks |
Accepted provider events
DALP acts only on Fireblocks transaction status events (transaction.status.updated and transaction.approval_status.updated) that carry a transaction id and a terminal status:
- A transaction that has cleared its authorization policy and is confirming or completed resolves the custody approval as approved.
- A transaction that was cancelled, rejected, blocked, failed, or timed out resolves it as rejected.
Intermediate, unrelated, or unrecognised events are acknowledged without changing the custody approval. DALP routes the decision to whichever approval is waiting on that transaction id, whether it is a provider-native broadcast approval or a sign-only approval.
Authentication and request limits
The Fireblocks callback body is capped at 64 KB before DALP verifies a signature or parses JSON. Requests over the cap return 413.
The route is unauthenticated: no API key or bearer token is required to call it. DALP verifies the detached JWS in the Fireblocks-Webhook-Signature header against the Fireblocks public keys published at the workspace's JWKS endpoint. The JWKS URL is region-specific, so configure the URL that matches the Fireblocks workspace. Key rotation is automatic because the signature header selects the signing key. A request whose JWS does not verify is accepted with 200 and is not acted on. An unverifiable call cannot resolve a custody approval. Fireblocks does not retry a payload that can never succeed.
Provider-facing status codes
Fireblocks receives one of the following responses.
| Condition | Response | Effect |
|---|---|---|
| Valid terminal status resolved to the custody approval | 200 | Delivery accepted |
| Intermediate, unrecognised, or already-resolved event | 200 | Delivery acknowledged without changing the approval |
| Signature does not verify, or the payload is not valid JSON | 200 | Delivery acknowledged without changing the approval |
| Body exceeds 64 KB | 413 | Delivery rejected as too large |
| Restate or the approval monitor is unreachable, or a retryable transport error occurs | 503 | Provider can retry delivery |
Because an unverifiable or malformed body returns 200, the polling backstop remains the safety net for any decision a webhook never delivers.
Related pages: Custody providers, Signing flow, Operational integration patterns.
DFNS Custody inbound callbacks
POST /api/webhooks/dfns is the route DFNS Custody calls to report signing and approval events. DFNS calls this route when a signature request or its approval policy reaches a final decision. DALP reads the terminal decision and resolves the matching custody approval, so a pending DFNS signature clears on the provider's push instead of waiting on the next poll cycle.
You do not call this route. The platform exposes the callback URL to your DFNS application, and the provider delivers events to it.
Like the Ripple and Fireblocks callbacks, this route is separate from outbound webhook endpoints at POST /api/v2/webhooks. The table below shows each direction, its purpose, and the party that configures the URL.
| Direction | Purpose | Who configures the URL | Public API surface |
|---|---|---|---|
| Outbound webhook endpoint | DALP sends selected platform events to your HTTPS receiver | You create and manage the receiver URL | /api/v2/webhooks and child routes |
| DFNS Custody callback | DFNS sends signing and approval decisions back to DALP | The platform exposes the DALP callback URL to your DFNS application | POST /api/webhooks/dfns |
Accepted provider events
DALP resolves the custody approval from a terminal DFNS event that carries a signature id:
- A completed signature event, or an approved policy decision for a policy-gated sign, resolves the custody approval as approved.
- A rejected, failed, or denied signature or policy event resolves it as rejected.
Both signing paths are covered: provider-native signature events and policy-gated signs that finish with an approval-policy decision rather than a raw signature event. Intermediate or unrecognised events are acknowledged without changing the custody approval, and so are events without a recognisable signature id.
Authentication and request limits
The DFNS callback body is capped at 64 KB before DALP verifies a signature or parses JSON. Requests over the cap return 413.
The route is unauthenticated: no API key or bearer token is required to call it. DALP verifies an HMAC-SHA256 signature from the x-dfns-webhook-signature header against the configured DFNS webhook secret, using a constant-time comparison. The value is accepted with or without a leading sha256= prefix. A request whose HMAC does not verify is accepted with 200 and is not acted on. An unverifiable call cannot resolve a custody approval. DFNS does not retry a payload that can never succeed.
If the DFNS webhook secret is not configured, DALP cannot verify incoming calls and returns 503 so DFNS retries after the secret is set, rather than dropping a terminal approval.
Provider-facing status codes
DFNS receives one of the following responses.
| Condition | Response | Effect |
|---|---|---|
| Valid terminal event resolved to the custody approval | 200 | Delivery accepted |
| Intermediate, unrecognised, or unmatched event | 200 | Delivery acknowledged without changing the approval |
| Signature does not verify, or the payload is not valid JSON | 200 | Delivery acknowledged without changing the approval |
| Body exceeds 64 KB | 413 | Delivery rejected as too large |
| Webhook secret not configured, or a retryable dispatch error occurs | 503 | Provider can retry delivery |
Because an unverifiable or malformed body returns 200, the polling backstop remains the safety net for any decision a webhook never delivers.
Related pages: Custody providers, Signing flow, Operational integration patterns.
Counter-signed receipts
Enable counterSignedReceipts when the receiving system must prove that it accepted a delivered event. DALP accepts signed acknowledgements only for deliveries queued with that flag enabled. Each record stores the submitted signature, inner event hash, receipt time, and verification status.
| Operation | API route |
|---|---|
| List counter-signed webhook receipts | GET /api/v2/webhook-receipts |
| Read one counter-signed webhook receipt | GET /api/v2/webhook-receipts/{id} |
| Submit a consumer counter-signed receipt | POST /api/v2/webhook-receipts |
A receipt submission includes deliveryId, evtId, endpointId, consumerSignature, and innerEventHash. The consumer signature is an HMAC-SHA256 over a canonical signing string that binds the full delivery tuple: the dalp.whr.v1 scheme tag followed by deliveryId, endpointId, evtId, and innerEventHash. Binding the signature to that tuple means a receipt proves which delivery the consumer acknowledged, and a signature minted for one delivery attempt cannot be replayed onto another delivery of the same event. Generate it with the endpoint signing secret after removing the dalp_whsk_ prefix, keying the HMAC on that prefix-stripped string directly. See the webhook receipts API reference for the exact signing string. DALP also compares the submitted hash with the delivered event body, so a receipt only verifies when the signature and payload hash both match.
Receipt window
The receipt window is short. After delivering an event to a counter-signed endpoint, DALP waits a default of 30 seconds, and never more than 60 seconds, for the consumer to submit its acknowledgement. Submit as part of handling the event rather than on a deferred or batched schedule.
Late receipts are rejected after the window closes, and the attempt is recorded with the RECEIPT_TIMEOUT failure class. Because RECEIPT_TIMEOUT is retried automatically, a consumer that misses the window should acknowledge the next retry instead of trying to repair the timed-out attempt.
Audit proof
DALP records delivery rows with the fields that were redacted during delivery preparation. It also records hop hashes for the prepared payload.
Use GET /api/v2/webhooks/events/{evtId}/chain-of-custody when an audit workflow needs proof for one event. The response uses the standard single-resource envelope:
{
"data": {
"evtId": "evt_01h...",
"hops": [
{
"stage": "outbox-write",
"contentHash": "sha256:4f7c...",
"signedBy": "0x...",
"recordedAt": "2026-05-13T07:32:00.000Z"
}
],
"merkleRoot": "7a1f...",
"platformSignature": "dalp-platform:7a1f..."
},
"links": {
"self": "/v2/webhooks/events/evt_01h.../chain-of-custody"
}
}The hops array lists the recorded delivery stages and content hashes. signedBy is present when a hop has a recorded signer. Downstream systems can compare the hop hashes with the merkleRoot and platform signature when they need evidence of what DALP prepared for delivery.
Related pages:
Reporting and audit access
Use DALP's indexed read APIs, dashboard exports, transaction references, webhook delivery records, and audit-access responsibilities for reporting and data retrieval.
Webhook event integration overview
Subscribe to DALP lifecycle events with AsyncAPI, signed webhooks, idempotent delivery, and typed SDK verification.