Immersve notifies your integrated client about domain events, such as KYC results, card status changes, and payment updates, by sending HTTP Webhook requests to your Webhook Listener endpoint. This guide covers the notification format, how to verify a delivery, what your listener must return, and the delivery guarantees you can rely on. See Configuring Webhook Listeners for the steps to register a listener and subscribe it to topics.
Requirements
Before Immersve can deliver notifications to your systems:
-
Handle Subscribed Topics — Your listener must accept
POSTrequests at<url>/<topic>for every topic you subscribe to. -
HTTPS Listener Endpoints — Your Webhook Listener's URL must use HTTPS.
-
Idempotent Handling — Your listener must be safe to invoke more than once for the same notification. See Idempotency.
Available Topics
See Webhook Topics for the topic names and exact payload schemas for all available Webhook Topics.
Protocol
A notification is delivered as an HTTP POST to your Webhook Listener's registered URL with the topic name appended as a path segment — https://example.com/webhooks becomes https://example.com/webhooks/kyc-succeeded. The request body is the JSON Envelope described below and includes a payload attribute carrying the fields specific to the topic.
Every topic defines its own payload shape — a KYC notification carries accountId and kycStatus, a payment notification carries a payment object, and so on. See Available Topics for the full list of topics and their reference pages, each documenting its exact payload schema and a sample.
Envelope
Every notification wraps its topic-specific payload in a fixed set of metadata fields. This wrapper is the Envelope — the same shape regardless of topic.
{ "messageId": "dcd07ae53c03209282543316312e4c38", "topic": "kyc-succeeded", "listenerId": "9ee479ec4dd3a2cd77a4fb4c8ffe51ec", "listenerAccountId": "2d4aec21adf5c3251d6f868accf94791", "deliveryAttempt": 1, "createdAt": "2026-08-17T01:46:14.481Z", "sentAt": "2026-08-17T01:46:14.528Z", "keyId": "c1e1a59e41d9e009a43256cab1f8839d", "issuer": "api.immersve.com", "payload": { "accountId": "00000000-0000-0000-0000-000000000000", "kycStatus": "succeeded" }}The following table describes each field in the envelope.
| Field | Type | Description |
|---|---|---|
messageId | string | Unique id for this notification. Stable across every delivery attempt for the same event — use it to deduplicate. See Idempotency. |
topic | string | Name of the Webhook Topic that triggered this notification, for example kyc-succeeded. See Available Topics. |
listenerId | string | Id of the Webhook Listener this notification was sent to. |
listenerAccountId | string | Id of the partner account that owns the listener. |
deliveryAttempt | number | 1-based count of delivery attempts made for this message. Changes on every retry. |
createdAt | string | Timestamp the notification was created, in ISO 8601. |
sentAt | string | Timestamp this delivery attempt was sent, in ISO 8601. Differs from createdAt on a retry. Can help detect a replayed request — see Idempotency. |
keyId | string | Id of the public key used to sign this delivery. See Security. |
issuer | string | Hostname of the Immersve environment that sent the notification — api.immersve.com in Live, test.immersve.com in Test. |
payload | object | Topic-specific data. Its shape depends on topic — see Protocol and Available Topics. |
Security
Every delivery is signed so you can confirm it came from Immersve and that the body was not altered in transit.
| Header | Description |
|---|---|
X-Delivery-ID | Unique message id plus delivery attempt, in the form {messageId}:{deliveryAttempt}. |
X-Key-ID | Id of the public key used to sign this delivery. |
X-Signature | Base64-encoded RSA SHA-256 signature of {deliveryId}:{keyId}:{requestBody}. |
To verify a delivery:
- Fetch Immersve's public keys from
https://${imsv_api_host}/.well-known/jwks.json— an unauthenticatedGETreturning a standard JWKS document. - Find the key whose
kidmatches the request'sX-Key-IDheader. - Reconstruct the signed string as
{X-Delivery-ID}:{X-Key-ID}:{raw request body}, using the exact raw body bytes as received. - Verify the base64-decoded
X-Signatureagainst that string with the matched public key, using RSA SHA-256.
const crypto = require('node:crypto');
async function verifyWebhookSignature(request, jwksUrl) { const deliveryId = request.headers['x-delivery-id']; const keyId = request.headers['x-key-id']; const signature = request.headers['x-signature'];
const { keys } = await fetch(jwksUrl).then(res => res.json()); const jwk = keys.find(key => key.kid === keyId); const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' });
const signedString = `${deliveryId}:${keyId}:${request.rawBody}`; return crypto.verify( 'sha256', Buffer.from(signedString), publicKey, Buffer.from(signature, 'base64'), );}Cache the JWKS response rather than fetching it on every delivery — keys are identified by kid/keyId, so a cached set stays valid until a key you haven't seen before appears.
Reject any request with a missing or invalid signature, and never process a payload before verifying it.
Response
A delivery attempt succeeds when your listener responds with an HTTP status in the 200–299 range within 15 seconds of the request being sent. Immersve does not inspect the response body and will not retry a delivery once it succeeds — respond with any 2xx status only once you are ready to consider the notification complete.
Any other outcome — a non-2xx status, a connection error, or no response within 15 seconds — is treated as a failed delivery attempt and handled as described in Delivery Guarantees.
Delivery Guarantees
Immersve delivers each notification at least once — the same notification can arrive more than once, and notifications for the same resource are not guaranteed to arrive in the order the underlying events occurred. Design your listener to tolerate both.
A failed delivery attempt (see Response) is retried automatically with increasing backoff. If no attempt succeeds within 24 hours of the notification's creation, Immersve stops retrying and the notification is abandoned — check deliveryStatus via List Webhook Notifications if you suspect this happened.
Most topics are delivered asynchronously and retried on failure as described above. A small number of topics — currently Payment 3DS OTP and the test-only transactional-test topic — are delivered synchronously instead of through the retry queue, and a failed attempt is not retried. Because there is no retry safety net for these, make sure your listener for these topics responds reliably and promptly.
Idempotency
Because delivery is at-least-once, your listener must be safe to invoke more than once for the same event. Use the Envelope's messageId as the deduplication key — it is stable across every delivery attempt for a given notification. Track the messageId values you've already processed and skip repeats.
X-Delivery-ID (and the envelope's deliveryAttempt) changes on every retry of the same notification, so don't use it alone for deduplication — it identifies the attempt, not the event.
Retaining the messageId ledger indefinitely also protects against a captured request being replayed later — a valid signature does not expire, so X-Signature verification alone cannot detect a replay. If you don't retain the ledger indefinitely, you can treat an implausibly old sentAt as a secondary signal that a request may be a replay. Because Immersve's own retries can span up to 24 hours (see Delivery Guarantees), any threshold needs to be generous — this is a supplement to messageId deduplication, not a replacement for it.
Best Practices
-
Verify Every Signature — Always verify
X-Signaturebefore trusting a payload; never process an unsigned or invalid request. -
Acknowledge When Complete — Respond with a 2xx only once you are ready to consider the notification complete. Immersve does not retry a delivery after a successful response, so acknowledging earlier risks losing the notification if your processing fails afterward.
-
Deduplicate on
messageId— Treat delivery as at-least-once and make handlers idempotent. -
Tolerate Out-of-Order Delivery — Reconcile using resource state or timestamps in the payload, not the order notifications arrive.
-
Respond Within Deadline — Your listener should respond within 15 seconds, before the request times out. See Response.
-
Test Every Topic Before Going Live — Use Send Webhook Test Notification to exercise your listener with each topic's sample payload, including the test-only
async-testandtransactional-testtopics, before relying on it in production. -
Check Delivery Status When in Doubt — Use List Webhook Notifications to inspect
deliveryStatusfor a listener rather than assuming an event never fired.
Troubleshooting
Common issues are described below by symptom, along with the likely cause and the fix.
Never Receive Notifications for a Topic
- Likely Cause — No active Webhook Topic Subscription for that topic on the listener.
- Fix — Confirm subscriptions with Get Webhook Listener, then create one with Add Webhook Topic Subscription.
Signature Verification Fails
- Likely Cause — Verifying against the wrong string, the wrong public key, or a re-serialized body.
- Fix — Verify against the exact raw request body bytes and the string
{deliveryId}:{keyId}:{requestBody}; matchX-Key-IDto the JWKSkid, not a cached older key.
The Same Notification Arrives More Than Once
- Likely Cause — Expected at-least-once delivery.
- Fix — Deduplicate on
messageId— see Idempotency.
Notifications for One Resource Arrive Out of Order
- Likely Cause — Expected. Delivery order is not guaranteed.
- Fix — Sequence using resource state or payload timestamps, not arrival order.
A Notification Stops Retrying and Never Arrives
- Likely Cause — Delivery was abandoned after the 24-hour retry window.
- Fix — Check
deliveryStatusvia List Webhook Notifications and look for non-2xx responses or timeouts on earlier attempts.
One Topic Always Fails, Others Succeed
- Likely Cause — Your listener doesn't have a working route at
<url>/<topic>for that specific topic. - Fix — Confirm your handler covers every subscribed topic's path independently.