Skip to main content

Create a webhook subscription

POST 

/v1/webhooks

Register an HTTPS endpoint to receive signed event notifications. Requires the connector:webhooks:create scope and an Idempotency-Key header.

Request​

  • url must be an absolute https:// URL with a host. Plain http:// is rejected.
  • delivery_format must be jsonapi. iso20022+xml is reserved and currently rejected with 400 INVALID_FIELD_FORMAT.
  • security.signature.secret is a shared secret that your bank chooses (the platform does not generate one). It is write-only and is never returned. Use a long random value (for example 32 random bytes, hex or base64 encoded). The platform uses the UTF-8 bytes of this string, exactly as sent, as the HMAC key.
  • security.signature.secret_version is your label for this secret (1 to 64 printable ASCII characters, no spaces, for example v1). It is echoed on every delivery in the X-DAN-Secret-Version header so you know which secret to verify with.
  • event_types lists the event types to receive. Values are matched exactly. An empty array receives no business events (only the verification handshake). Values are not validated: a misspelled type is accepted but never matches.

What happens next​

The response is 202 Accepted with an operation. The subscription is created immediately: its webhook_id is the relationships.resource.data.id of GET /v1/operations/{operation_id}. The new subscription starts in PENDING_VERIFICATION and the platform immediately sends one webhook.verification event to url (the verification handshake). When your endpoint answers it with any 2xx status, the subscription becomes ACTIVE and starts receiving the event types it subscribed to. Events that occur while a subscription is not ACTIVE are not delivered to it later.

Retrying the request with the same Idempotency-Key and body returns the same 202 document and does not create a second subscription.

Event types​

Event typeSent whenresource.type
operation.updatedan operation is accepted, starts processing, or starts waiting for ledger commitsoperations
operation.manual_reviewan operation starts waiting for an approval review (operation state PENDING_REVIEW)operations
operation.succeededan operation completes successfullyoperations
operation.rejectedan operation is rejected (for example by a review)operations
operation.failedan operation failsoperations
review.createda new approval review is openedreviews
review.updatedan approval was recorded but more approvals are still neededreviews
review.approveda review reached its approval thresholdreviews
review.rejecteda review was rejectedreviews
transfer.receiveda transfer to your bank (as destination holder bank) completed; carries the parties' registered accounts and any sender (originator) and beneficiary details the sender submitted, for your bank to screen, resolve and credittransfers
beneficiary.screening.requestedyour bank is asked to screen the beneficiary of an incoming transfertransfers
webhook.verificationhandshake, sent to a subscription in PENDING_VERIFICATION; not subject to event_typeswebhooks
webhook.testsynthetic test event requested through POST /v1/webhooks/{webhook_id}/testwebhooks

Each business event is delivered to every ACTIVE subscription of the bank the event belongs to that lists its type. The exact body of each event is shown in the webhookDelivery callback below.

Delivery request​

Each delivery is an HTTPS POST to url with a compact JSON body (Content-Type: application/json) and these headers:

HeaderValue
X-DAN-Event-IdUUID of the event. Part of the signed string.
X-DAN-Event-TypeEvent type, for example operation.succeeded.
X-DAN-Webhook-IdUUID of the subscription.
X-DAN-Delivery-IdUUID of this delivery (one per event and subscription; the same on every retry).
X-DAN-TimestampTime the request was signed, RFC 3339 in UTC, for example 2026-09-26T10:15:30.482913+00:00. New on every attempt.
X-DAN-Secret-Versionsecret_version of the secret that produced the signature.
X-DAN-Signaturev1= followed by the lowercase hex HMAC-SHA256 signature.
AuthorizationBearer <access token>, only when an auth profile is bound.

The platform only connects over HTTPS (TLS 1.2 or later, certificate verified), does not follow redirects, and refuses URLs that resolve to loopback, link-local, multicast or unspecified addresses. Connect timeout is 10 seconds; the whole request must complete within 30 seconds.

Verifying the signature​

The signature is HMAC-SHA256(key = secret, message = X-DAN-Timestamp + "." + X-DAN-Event-Id + "." + raw request body), hex encoded in lowercase and prefixed with v1=. Always compute it over the raw body bytes as received, before any JSON parsing or re-serialisation. Compare in constant time. Pick the secret by the X-DAN-Secret-Version header. The platform does not reject old timestamps for you; we recommend that you reject requests whose X-DAN-Timestamp is more than 5 minutes from your clock, to limit replay (retries are re-signed with a fresh timestamp, so this does not break retries).

Python:

import hmac, hashlib
from datetime import datetime, timezone

SECRETS = {"v1": b"your-webhook-secret"} # keyed by secret_version

def verify(headers, raw_body: bytes) -> bool:
secret = SECRETS.get(headers["X-DAN-Secret-Version"])
if secret is None:
return False
ts = headers["X-DAN-Timestamp"]
age = datetime.now(timezone.utc) - datetime.fromisoformat(ts)
if abs(age.total_seconds()) > 300:
return False
message = ts.encode() + b"." + headers["X-DAN-Event-Id"].encode() + b"." + raw_body
expected = "v1=" + hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-DAN-Signature"])

Node.js:

const crypto = require("crypto");
const SECRETS = { v1: "your-webhook-secret" }; // keyed by secret_version

function verify(headers, rawBody /* Buffer */) {
const secret = SECRETS[headers["x-dan-secret-version"]];
if (!secret) return false;
const ts = headers["x-dan-timestamp"];
if (Math.abs(Date.now() - Date.parse(ts)) > 5 * 60 * 1000) return false;
const expected = "v1=" + crypto.createHmac("sha256", secret)
.update(`${ts}.${headers["x-dan-event-id"]}.`)
.update(rawBody)
.digest("hex");
const given = String(headers["x-dan-signature"] || "");
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Responding, retries and duplicates​

  • Answer with any 2xx status to acknowledge. The response body is ignored. Respond quickly and process asynchronously.
  • 429, any 5xx, a timeout or a connection error is retried. The retry schedule is set by the platform deployment; in the standard configuration a delivery is attempted at most 5 times, with increasing delays between attempts (currently 1, 5, 15 and 30 minutes). After the last failed attempt the delivery is marked FAILED and is not retried again.
  • Any other status (3xx, 400, 401, 403, 404 and other 4xx) is a permanent failure: the delivery is marked FAILED immediately and not retried.
  • Delivery is at least once: the same event can arrive more than once (for example after a timeout on a request you actually processed). De-duplicate on the body id (or X-DAN-Delivery-Id), which stays the same across retries.
  • Ordering is not guaranteed, across events or even for the same resource, because retries interleave with newer events. Use occurred_at to order events, and fetch the resource (data.links.self) when you need its current state.
  • Inspect delivery history with GET /v1/webhooks/deliveries?filter[webhook_id]=....

Request​

Responses​

Accepted. The operation (data.id) tracks the creation; poll GET /v1/operations/{operation_id} to read the new webhook_id from relationships.resource. The verification handshake is sent right after.

Callbacks​

POST 

{$request.body#/data/attributes/url}

The request the platform sends to your subscription url for every event. Verify X-DAN-Signature over the raw body before trusting it (see POST /v1/webhooks for the algorithm and code samples), then answer 2xx.

Every event shares one envelope:

  • id: event identifier, evt_ followed by a UUID. Stable across retries of the same delivery; use it to de-duplicate.
  • type: the event type (also sent in X-DAN-Event-Type).
  • occurred_at: when the underlying change happened (RFC 3339, UTC).
  • bank_id: the bank the event belongs to (your bank).
  • resource: type and id of the resource the event is about.
  • data: a JSON:API-style resource object (type, id, attributes, and for most events links.self, the API path to fetch the full resource).
  • meta.delivery_semantics: always AT_LEAST_ONCE.

Keys are serialised in alphabetical order without whitespace. Do not rely on key order; always verify the signature over the bytes you received.

Callbacks Responses​

Retried later, until the attempt limit is reached.