3DS Authentication
Handle 3D Secure authentication for card transactions.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain access tokens |
| Onboarding | User and wallet registration |
| KYC | KYC verification requirements |
Overview
3D Secure (3DS) is an authentication protocol that adds an extra layer of security for card transactions. When a transaction requires 3DS verification, the user sees a 3DS window with two authentication options:
OTP Code (SMS)
The user receives an OTP code via SMS to the phone number provided during registration. The user enters the code directly in the 3DS form. This flow requires no backend interaction from your system.
In-App Confirmation
The user chooses to confirm the transaction in your app. This triggers a webhook to your system, and you must prompt the user to approve or decline. This document covers the in-app confirmation flow.
Flow
1. User makes card payment at merchant
↓
2. Issuer determines 3DS is required
↓
3. Wirex sends webhook to your system
↓
4. Your app displays approval UI to user
↓
5. User approves or declines
↓
6. Your app calls approve/decline endpoint
↓
7. Transaction proceeds or is blocked
Webhook
When a transaction requires 3DS authentication, Wirex sends a webhook to your configured endpoint.
Endpoint: POST {your_webhook_base_url}/v2/webhooks/3ds
{
"card_id": "64120850-73a1-4df5-a074-d463258c9deb",
"owner": "0x1234567890abcdef1234567890abcdef12345678",
"transaction_id": "00000000000000000000000000000001",
"merchant_name": "Amazon",
"amount": "100.00",
"currency": "USD",
"card_last_4": "1234"
}| Field | Description |
|---|---|
card_id | UUID of the card |
owner | User's wallet address |
transaction_id | Unique transaction identifier (use for approve/decline) |
merchant_name | Merchant name |
amount | Transaction amount |
currency | Transaction currency |
card_last_4 | Last 4 digits of card number |
Get Pending 3DS Requests
Retrieve all pending 3DS requests for a user. Use this to show pending approvals if the user missed the webhook notification.
GET /api/v1/cards/3ds/requests
const response = await fetch(`${baseUrl}/api/v1/cards/3ds/requests`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId
}
});
const requests = await response.json();response = requests.get(
f"{base_url}/api/v1/cards/3ds/requests",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id
}
)
pending_requests = response.json()req, _ := http.NewRequest("GET", baseURL+"/api/v1/cards/3ds/requests", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var requests []CardTransactionConfirmation
json.NewDecoder(resp.Body).Decode(&requests)Response:
[
{
"card_id": "64120850-73a1-4df5-a074-d463258c9deb",
"owner": "0x1234567890abcdef1234567890abcdef12345678",
"transaction_id": "00000000000000000000000000000001",
"merchant_name": "Amazon",
"amount": "100.00",
"currency": "USD",
"card_last_4": "1234"
}
]Approve Transaction
When the user confirms the transaction is legitimate, call the approve endpoint.
POST /api/v1/cards/3ds/requests/{transactionId}/approve
| Parameter | Description |
|---|---|
transactionId | Transaction ID from webhook or GET request |
await fetch(`${baseUrl}/api/v1/cards/3ds/requests/${transactionId}/approve`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId
}
});requests.post(
f"{base_url}/api/v1/cards/3ds/requests/{transaction_id}/approve",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id
}
)req, _ := http.NewRequest("POST", baseURL+"/api/v1/cards/3ds/requests/"+transactionId+"/approve", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
http.DefaultClient.Do(req)Response: Empty on success (200 OK)
Decline Transaction
When the user does not recognize the transaction or wants to block it, call the decline endpoint.
POST /api/v1/cards/3ds/requests/{transactionId}/decline
| Parameter | Description |
|---|---|
transactionId | Transaction ID from webhook or GET request |
await fetch(`${baseUrl}/api/v1/cards/3ds/requests/${transactionId}/decline`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId
}
});requests.post(
f"{base_url}/api/v1/cards/3ds/requests/{transaction_id}/decline",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id
}
)req, _ := http.NewRequest("POST", baseURL+"/api/v1/cards/3ds/requests/"+transactionId+"/decline", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
http.DefaultClient.Do(req)Response: Empty on success (200 OK)
Implementation Example
Complete flow handling in your application:
// 1. Webhook handler (your server)
app.post('/v2/webhooks/3ds', async (req, res) => {
const { transaction_id, merchant_name, amount, currency, card_last_4, owner } = req.body;
// Store request and notify user (push notification, in-app alert, etc.)
await notifyUser(owner, {
transaction_id,
merchant_name,
amount,
currency,
card_last_4
});
res.status(200).send();
});
// 2. User approval handler (your client)
async function handle3dsDecision(transactionId, approved) {
const action = approved ? 'approve' : 'decline';
const response = await fetch(
`${baseUrl}/api/v1/cards/3ds/requests/${transactionId}/${action}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId
}
}
);
if (response.ok) {
showMessage(approved ? 'Transaction approved' : 'Transaction declined');
}
}# 1. Webhook handler (your server)
@app.route('/v2/webhooks/3ds', methods=['POST'])
def handle_3ds_webhook():
data = request.json
# Store request and notify user
notify_user(data['owner'], {
'transaction_id': data['transaction_id'],
'merchant_name': data['merchant_name'],
'amount': data['amount'],
'currency': data['currency'],
'card_last_4': data['card_last_4']
})
return '', 200
# 2. User approval handler
def handle_3ds_decision(transaction_id: str, approved: bool):
action = 'approve' if approved else 'decline'
response = requests.post(
f"{base_url}/api/v1/cards/3ds/requests/{transaction_id}/{action}",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id
}
)
return response.ok// 1. Webhook handler (your server)
func handle3dsWebhook(w http.ResponseWriter, r *http.Request) {
var req CardTransactionConfirmation
json.NewDecoder(r.Body).Decode(&req)
// Store request and notify user
notifyUser(req.Owner, req)
w.WriteHeader(http.StatusOK)
}
// 2. User approval handler
func handle3dsDecision(transactionId string, approved bool) error {
action := "decline"
if approved {
action = "approve"
}
req, _ := http.NewRequest("POST",
baseURL+"/api/v1/cards/3ds/requests/"+transactionId+"/"+action, nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}UI Requirements
When displaying 3DS approval to users, show:
- Merchant name
- Transaction amount and currency
- Last 4 digits of the card
- Clear Approve and Decline buttons
- Timeout warning (3DS requests expire)
Error Handling
Success
| Status | Meaning |
|---|---|
| 200 | Decision recorded successfully |
Validation Errors (400)
{
"error_reason": "ErrorMissingField",
"error_description": "transaction id is required",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "transaction_id" }
]
}Authorization Errors (401)
Returned when the authenticated user is not the owner of the 3DS request.
{
"error_reason": "ErrorPermissionDenied",
"error_description": "3DS request owner mismatch",
"error_category": {
"category": "CategoryUnauthorized",
"http_status_code": 401
},
"error_details": [
{ "key": "field", "details": "owner" }
]
}Service Errors (500)
| Error Description | Cause |
|---|---|
| Failed to query active request by transaction id | Transaction ID not found or expired |
| Failed to approve 3DS request | 3DS service unavailable |
| Failed to decline 3DS request | 3DS service unavailable |
Updated 8 days ago
