Subscribe to 3-DS OTP

This guide walks through the complete 3-D Secure OTP integration — creating a subscription, handling the webhook callback, retrieving the OTP, delivering it to the cardholder, and unsubscribing. For the conceptual background on how 3DS works and why the callback is designed the way it is, see 3-D Secure.

Prerequisites

  • 3-D Secure must be enabled on your Program via Account Management. Cards on a Program without 3DS enabled will not receive OTP events regardless of subscription status. Confirm with your Axys account team if you're unsure.
  • The card must have issuerCardStatus = 1 (Active). See Manage the Card Lifecycle.
  • If using the webhook path: a publicly accessible HTTPS endpoint with a valid (non-self-signed) TLS certificate.

Overview

sequenceDiagram
    participant You as Your server
    participant Axys
    participant Cardholder
    participant Merchant

    Note over You,Axys: SETUP — once per card
    You->>Axys: POST /otp/listeners (cardId + optional webhookUrl)
    Axys->>You: { token, expiresAt: null, status: "success" }
    You->>You: Store token, keyed by cardId

    Note over Cardholder,Axys: AT CNP TRANSACTION TIME
    Cardholder->>Merchant: Submits card details online
    Merchant->>Axys: Authorization request — 3DS challenge triggered
    Axys->>You: Webhook callback: { cardId } — no OTP
    You->>You: Look up token for cardId
    You->>Axys: GET /otp/{token}
    Axys->>You: { status: "received", code: "847291", expiresAt: 1720000000 }
    You->>Cardholder: Deliver OTP (SMS / push / in-app)
    Cardholder->>Merchant: Enters OTP — transaction completes

Step 1: Create a subscription

POST /otp/listeners

By cardId (recommended)

{
  "cardId": 8821,
  "webhookUrl": "https://your-domain.example/callbacks/3ds-otp"
}

By cardholderId (legacy — covers all of the cardholder's cards)

{
  "cardholderId": 10532,
  "webhookUrl": "https://your-domain.example/callbacks/3ds-otp"
}
📘

Use cardId, not cardholderId. Subscribing by cardId creates a 1:1 mapping between a subscription token and a specific card — making the callback-to-OTP-retrieval flow unambiguous. cardholderId subscriptions are the original, legacy method and fan out to all of that cardholder's cards; one callback could relate to any of them, requiring additional matching logic on your side.

Response:

{
  "status": "success",
  "token": "otp_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresAt": null
}
Response fieldNotes
tokenOpaque, URL-safe token identifying this subscription. Store it, keyed by cardId. Treat it as a credential — encrypt at rest, never log.
expiresAtCurrently always null — the subscription itself does not expire. The field exists for potential future use.
❗️

Encrypt stored tokens at rest. The token grants the ability to retrieve OTPs for this card/cardholder and to unsubscribe. It must be treated as a sensitive credential: stored in a secrets store or encrypted database column, never logged, and rotated (by deleting and recreating the subscription) if potentially compromised.


Step 2: Handle the webhook callback

When a CNP transaction triggers a 3DS challenge for a subscribed card, Axys sends a POST request to your webhookUrl. The callback payload contains:

{
  "cardId": 8821
}

Or, for a cardholderId-level subscription:

{
  "cardholderId": 10532
}

The callback never contains the OTP code — this is a deliberate security design. See 3-D Secure: The security design for the full rationale.

Handling the callback

On receiving a callback:

  1. Look up the subscription token stored for the cardId (or cardholderId) in the payload.
  2. Call GET /otp/{token} immediately (the OTP expiresAt clock is already running).
  3. Deliver the OTP to the cardholder through your preferred channel.
  4. Respond to the callback with 2xx quickly — complete the OTP retrieval and delivery asynchronously if there's any latency risk in your pipeline.

Webhook endpoint requirements

RequirementNotes
HTTPS onlySelf-signed certificates will not be accepted
Publicly accessibleNot behind a firewall or IP restriction that blocks Axys's outbound IPs
Fast 2xx responseAcknowledge receipt quickly; process asynchronously if needed
IdempotentDesign the handler to be safe to process the same event more than once

Callback URLs are independent of your mTLS certificate — your webhook endpoint can be on a different domain from the one you use to call the API. See Webhooks & Callback Endpoints.


Step 3: Retrieve the OTP

GET /otp/{token}

Response:

{
  "status": "received",
  "code": "847291",
  "receivedAt": 1720000000,
  "expiresAt": 1720000300
}
statusMeaningAction
pendingSubscription exists — no OTP event has triggered yetDo not deliver; keep the subscription active
receivedAn OTP has been generated for an active 3DS challengeDeliver code to the cardholder before expiresAt
consumedOTP was used successfully to complete a transactionNo action needed
expiredexpiresAt was reached before the OTP was usedThe cardholder's transaction failed authentication; they may need to retry
🚧

Deliver before expiresAt. The OTP expiry is variable and enforced — if the cardholder doesn't receive and enter the code before expiresAt, the transaction will fail authentication. Keep your callback-to-delivery pipeline as fast as possible. expiresAt is a Unix timestamp in seconds — a value of 1720000300 means the code expires at that Unix time, not in 300 seconds from now.

Polling as an alternative to webhooks

If you don't configure a webhookUrl, you can poll GET /otp/{token} and act when status changes from pending to received. Note that:

  • The expiresAt clock on the OTP starts when the 3DS challenge is triggered, not when you first poll — you may retrieve a received status with very little time remaining.
  • Polling is unsuitable for latency-sensitive OTP delivery. The webhook approach is strongly preferred.

Step 4: Deliver the OTP to the cardholder

Deliver the code value through your preferred channel:

ChannelNotes
SMSMost universally expected by cardholders for card OTPs
Push notificationBest for mobile apps with notification permissions already granted
In-app displaySuitable where the cardholder is actively in your app at the time of purchase
EmailLess time-sensitive than SMS/push; use only if other channels unavailable

The OTP code (e.g., 847291) should be displayed prominently alongside the transaction context (merchant name if available, amount) so the cardholder can verify they initiated the transaction before entering it.


Step 5: Unsubscribe

DELETE /otp/{token}

Deletes the subscription identified by token. After deletion:

  • No further callbacks are sent for this subscription.
  • GET /otp/{token} will return 404.

Returns 404 if the token doesn't exist or has already been deleted.


Managing multiple subscriptions

ScenarioApproach
One subscription per cardRecommended — call POST /otp/listeners once with cardId per card that needs 3DS
Multiple subscribers for one cardCall POST /otp/listeners multiple times with the same cardId and different webhookUrl values — each creates an independent subscription token
Rotating the subscriptionCall POST /otp/listeners to create a new subscription, confirm it's working, then DELETE /otp/{old_token} to retire the old one
Subscribing all cards for a cardholderEither loop over the cardholder's cards and subscribe each by cardId, or use a single cardholderId subscription (legacy) — the former is preferred

Summary: endpoints

OperationEndpointKey notes
Create subscriptionPOST /otp/listenerscardId recommended; cardholderId legacy
Retrieve OTPGET /otp/{token}404 if token invalid/expired
Delete subscriptionDELETE /otp/{token}404 if already deleted

What's next