3DS Authentication

Handle 3D Secure authentication for card transactions.

Before You Start

Read the following guides before proceeding:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain access tokens
OnboardingUser and wallet registration
KYCKYC 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"
}
FieldDescription
card_idUUID of the card
ownerUser's wallet address
transaction_idUnique transaction identifier (use for approve/decline)
merchant_nameMerchant name
amountTransaction amount
currencyTransaction currency
card_last_4Last 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
ParameterDescription
transactionIdTransaction 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
ParameterDescription
transactionIdTransaction 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

StatusMeaning
200Decision 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 DescriptionCause
Failed to query active request by transaction idTransaction ID not found or expired
Failed to approve 3DS request3DS service unavailable
Failed to decline 3DS request3DS service unavailable