When a card payment triggers 3D Secure, the challenge is pushed to the customer’s open WebSocket connection — no polling needed.

The flow

1

Receive the challenge

A 3DS_CHALLENGE_INITIATED event arrives. Show the merchant name, amount, and currency in a blocking prompt (modal or bottom sheet) with Approve / Reject.
2

Acknowledge delivery

Send an ack over the WebSocket so the challenge isn’t re-delivered on reconnect:
{
  "action": "notifications.3ds.ack",
  "notificationId": "<data.id>",
  "providerRequestId": "<data.providerRequestId>"
}
3

Send the decision over REST

The decision is a state-changing action, so it goes through Confirm a 3DS challenge — a REST call with a confirmed HTTP response, not a fire-and-forget WebSocket message.
4

Get the result

A 3DS_CHALLENGE_RESULT event confirms the outcome and the transaction status updates.

3DS_CHALLENGE_INITIATED

{
  "code": "3DS_CHALLENGE_INITIATED",
  "message": "3DS authentication required. Please verify your transaction.",
  "data": {
    "id": "abc12345-6789-def0-1234-56789abcdef0",
    "accountId": "acc-9876-5432-10fe-dcba",
    "status": "PENDING",
    "currency": "EUR",
    "amount": 15000,
    "merchantName": "Global Tech Store - Paris",
    "createdAt": 1781000000000,
    "expiresAt": 1781000300000
  }
}
FieldMeaning
data.idThe challenge ID — pass it to the ack and the REST confirm.
data.amountMinor units (15000 = 150.00).
data.expiresAtEpoch ms — hide the prompt and treat as declined once passed.

3DS_CHALLENGE_RESULT

Pushed when the challenge reaches a final state (approved, rejected, or expired), with the same data.id. Use it to dismiss the prompt and refresh the transaction view.
If the customer was offline when the challenge fired, it’s re-delivered on reconnect (10-minute window) — so the prompt logic should tolerate receiving a challenge that’s already close to its expiresAt.