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 (recommended){
"cardId": 8821,
"webhookUrl": "https://your-domain.example/callbacks/3ds-otp"
}By cardholderId (legacy — covers all of the cardholder's cards)
cardholderId (legacy — covers all of the cardholder's cards){
"cardholderId": 10532,
"webhookUrl": "https://your-domain.example/callbacks/3ds-otp"
}Use
cardId, notcardholderId. Subscribing bycardIdcreates a 1:1 mapping between a subscription token and a specific card — making the callback-to-OTP-retrieval flow unambiguous.cardholderIdsubscriptions 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 field | Notes |
|---|---|
token | Opaque, URL-safe token identifying this subscription. Store it, keyed by cardId. Treat it as a credential — encrypt at rest, never log. |
expiresAt | Currently always null — the subscription itself does not expire. The field exists for potential future use. |
Encrypt stored tokens at rest. The
tokengrants 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:
- Look up the subscription
tokenstored for thecardId(orcardholderId) in the payload. - Call
GET /otp/{token}immediately (the OTPexpiresAtclock is already running). - Deliver the OTP to the cardholder through your preferred channel.
- Respond to the callback with
2xxquickly — complete the OTP retrieval and delivery asynchronously if there's any latency risk in your pipeline.
Webhook endpoint requirements
| Requirement | Notes |
|---|---|
| HTTPS only | Self-signed certificates will not be accepted |
| Publicly accessible | Not behind a firewall or IP restriction that blocks Axys's outbound IPs |
Fast 2xx response | Acknowledge receipt quickly; process asynchronously if needed |
| Idempotent | Design 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
}status | Meaning | Action |
|---|---|---|
pending | Subscription exists — no OTP event has triggered yet | Do not deliver; keep the subscription active |
received | An OTP has been generated for an active 3DS challenge | Deliver code to the cardholder before expiresAt |
consumed | OTP was used successfully to complete a transaction | No action needed |
expired | expiresAt was reached before the OTP was used | The 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 beforeexpiresAt, the transaction will fail authentication. Keep your callback-to-delivery pipeline as fast as possible.expiresAtis a Unix timestamp in seconds — a value of1720000300means 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
expiresAtclock on the OTP starts when the 3DS challenge is triggered, not when you first poll — you may retrieve areceivedstatus 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:
| Channel | Notes |
|---|---|
| SMS | Most universally expected by cardholders for card OTPs |
| Push notification | Best for mobile apps with notification permissions already granted |
| In-app display | Suitable where the cardholder is actively in your app at the time of purchase |
| Less 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 return404.
Returns 404 if the token doesn't exist or has already been deleted.
Managing multiple subscriptions
| Scenario | Approach |
|---|---|
| One subscription per card | Recommended — call POST /otp/listeners once with cardId per card that needs 3DS |
| Multiple subscribers for one card | Call POST /otp/listeners multiple times with the same cardId and different webhookUrl values — each creates an independent subscription token |
| Rotating the subscription | Call 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 cardholder | Either loop over the cardholder's cards and subscribe each by cardId, or use a single cardholderId subscription (legacy) — the former is preferred |
Summary: endpoints
| Operation | Endpoint | Key notes |
|---|---|---|
| Create subscription | POST /otp/listeners | cardId recommended; cardholderId legacy |
| Retrieve OTP | GET /otp/{token} | 404 if token invalid/expired |
| Delete subscription | DELETE /otp/{token} | 404 if already deleted |
What's next
- 3-D Secure (Core Concepts — background and full API reference)
- Webhooks & Callback Endpoints
- Manage the Card Lifecycle
