3DS2

3DS2 API Developer Guide

Accept secure card payments with 3D Secure 2.0 authentication for Strong Customer Authentication (SCA) compliance.


Overview

The Zai 3DS2 solution extends our existing Cards Payin Workflow to support 3D Secure 2.0 authentication, ensuring compliance with PSD2 Strong Customer Authentication requirements. Our JavaScript SDK seamlessly handles device data collection, challenge flows, and payment status polling.

Why 3DS2?

  • Fraud Reduction: Advanced authentication reduces chargebacks
  • Higher Authorization Rates: Smart risk assessment minimizes unnecessary challenges
  • Seamless UX: Frictionless authentication when possible

Key Features

  • Drop-in Integration: Works with existing Zai card payment flows
  • OAuth2 Authentication: Consistent with Zai's security model
  • Webhook Support: Real-time payment status notifications
  • Flexible Authentication: Support for frictionless and challenge flows
  • Secure iFrames: PCI-compliant data collection and challenges

Prerequisites

Before implementing 3DS2, ensure you have:


Quick Start

1. Create Users and Item

Follow the standard Zai workflow to set up your payment:

# Create buyer user
curl -X POST https://test.api.promisepay.com/users \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "buyer_123",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "country": "AUS"
  }'

# Create seller user
curl -X POST https://test.api.promisepay.com/users \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "seller_456", 
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "[email protected]",
    "country": "AUS"
  }'

# Create card account for buyer
curl -X POST https://test.api.promisepay.com/card_accounts \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "buyer_123",
    "full_name": "John Doe",
    "number": "4444111122223333",
    "expiry_month": "12",
    "expiry_year": "2025",
    "cvv": "123"
  }'

# Create item
curl -X POST https://test.api.promisepay.com/items \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "item_789",
    "name": "Product Purchase",
    "amount": 5000,
    "payment_type": 2,
    "buyer_id": "buyer_123",
    "seller_id": "seller_456",
    "description": "3DS2 test payment"
  }'

2. Initiate Payment with 3DS2 using new endpoint

Use the new endpoint for 3DS2 support:

curl -X PATCH https://test.api.promisepay.com/items/item_789/make_payment_async \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "card_account_id",
    "request_three_d_secure": "automatic"
  }'

Response:

{
  "payment_id": "1000014012533486",  # payment ID will be a UUID 
  "account_id": "725cc8c0-759b-0138-5d6d-0a58a9feac05",
  "payment_token": "sample-token-1234567890",
  "items": {
    "id": "7190770-1-2908",
    "name": "Item 7190770-1-2908",
    "description": "Test Item 7190770-1-2908",
    "custom_descriptor": null,
    "payout_descriptor": null,
    "created_at": "2020-05-11T10:26:49.660Z",
    "updated_at": "2020-05-11T10:26:49.660Z",
    "state": "pending",
    "net_amount": 102,
    "chargedback_amount": 0,
    "refunded_amount": 0,
    "released_amount": 0,
    "seller_url": "",
    "buyer_url": "",
    "remaining_amount": 102,
    "status": 22000,
    "amount": 102,
    "payment_type_id": 2,
    "due_date": null,
    "requested_release_amount": 0,
    "pending_release_amount": 0,
    "dynamic_descriptor": null,
    "invoice_url": null,
    "deposit_reference": "100014012533386",
    "buyer_fees": 0,
    "seller_fees": 0,
    "credit_card_fee": 0,
    "direct_debit_fee": 0,
    "paypal_fee": 0,
    "promisepay_fee": 1,
    "batch_state": null,
    "total_outstanding": 102,
    "total_amount": 102,
    "currency": "AUD",
    "payment_method": "pending",
    "buyer_name": "Neol Buyer",
    "buyer_email": "[email protected]",
    "buyer_country": "AUS",
    "seller_name": "Assembly seller71718579",
    "seller_email": "[email protected]",
    "seller_country": "AUS",
    "payment_credit_card_enabled": true,
    "payment_direct_debit_enabled": true,
    "tds_check_state": null,
    "related": {
      "buyers": "buyer-719013950014",
      "sellers": "seller-71718579"
    },
    "links": {
      "self": "/items/7190770-1-2908/make_payment?account_id=725cc8c0-759b-0138-5d6d-0a58a9feac05",
      "buyers": "/items/7190770-1-2908/buyers",
      "sellers": "/items/7190770-1-2908/sellers",
      "status": "/items/7190770-1-2908/status",
      "fees": "/items/7190770-1-2908/fees",
      "transactions": "/items/7190770-1-2908/transactions",
      "batch_transactions": "/items/7190770-1-2908/batch_transactions",
      "wire_details": "/items/7190770-1-2908/wire_details",
      "bpay_details": "/items/7190770-1-2908/bpay_details",
      "tds_checks": "/items/7190770-1-2908/tds_checks"
    }
  }

3. Initialise 3DS2 Authentication

Include the JavaScript SDK and handle authentication:

<!DOCTYPE html>
<html>
<head>
    <title>3DS2 Payment</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div id="payment-container">
        <h3>Processing Payment...</h3>
        <div id="status-indicator">Initializing...</div>
        <!-- 3DS2 iframes will be inserted here -->
    </div>

    <script src="https://hosted-fields.assemblypay.com/assembly-3ds2.js"></script>
    <script>
       (new assembly3DS2()).initialize({
            authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",  // payment_token
            paymentId: "1000014012533486",                              // payment_id
            env: "pre-live",                                            // "pre-live" or "production"
            onComplete: handleComplete,
            onError: handleError,
            targetElementId: "container-3ds2"


        });

        function handleComplete() {
             alert("3DS2 flow completed");
             // check item status in the API to confirm final outcome
            }
        }

       	function handleError(error) {
						alert("3DS2 flow failed: " + error.message);
            // check item status in API to ensure payment has failed, before trying again
        }
    </script>
</body>
</html>

Authentication

For authentication setup and token management, see our comprehensive Authentication Guide.

The 3DS2 front-end component uses a separate token from the one used to access the API. This token is short-lived, and grants access to the status of a single payment only.


API Reference

Payment Endpoints Comparison

Zai provides two payment endpoints to handle different authentication requirements:

Featuremake_paymentmake_payment_async
Flow TypeSynchronousAsynchronous
3DS2 Support❌ No✅ Yes
ResponseFinal payment resultPayment acknowledgment + status token
Use CaseSimple card payments3DS2 authentication required
IntegrationDirect API responseRequires 3DS2 SDK + polling
EnvironmentAllPre-live

non 3DS2 Payments

For non-3DS2 transactions, use the traditional make_payment endpoint:

PATCH /items/{item_id}/make_payment

Parameters:

ParameterTypeRequiredDescription
account_idstringCard account ID for payment
device_idstringDevice information for fraud detection
ip_addressstringIP address for fraud detection
cvvstringCVV verification

3DS2 Payments

For payments requiring 3DS2 authentication:

PATCH /items/{item_id}/make_payment_async

Parameters:

ParameterTypeRequired
account_idstring
request_three_d_securestring

Authentication Modes:

  • automatic (default): Zai determines if 3DS2 is required based on risk analysis
  • challenge: Request a 3DS2 challenge to be presented to user
  • any: Allow frictionless or challenge flow based on issuer response

Response:

{
  "payment_id": "1000014012533486",
  "account_id": "725cc8c0-759b-0138-5d6d-0a58a9feac05",
  "payment_token": "sample-token-1234567890",
  "items": {
    "id": "7190770-1-2908",
    "name": "Item 7190770-1-2908",
    "description": "Test Item 7190770-1-2908",
    "custom_descriptor": null,
    "payout_descriptor": null,
    "created_at": "2020-05-11T10:26:49.660Z",
    "updated_at": "2020-05-11T10:26:49.660Z",
    "state": "pending",
    "net_amount": 102,
    "chargedback_amount": 0,
    "refunded_amount": 0,
    "released_amount": 0,
    "seller_url": "",
    "buyer_url": "",
    "remaining_amount": 102,
    "status": 22000,
    "amount": 102,
    "payment_type_id": 2,
    "due_date": null,
    "requested_release_amount": 0,
    "pending_release_amount": 0,
    "dynamic_descriptor": null,
    "invoice_url": null,
    "deposit_reference": "100014012533386",
    "buyer_fees": 0,
    "seller_fees": 0,
    "credit_card_fee": 0,
    "direct_debit_fee": 0,
    "paypal_fee": 0,
    "promisepay_fee": 1,
    "batch_state": null,
    "total_outstanding": 102,
    "total_amount": 102,
    "currency": "AUD",
    "payment_method": "pending",
    "buyer_name": "Neol Buyer",
    "buyer_email": "[email protected]",
    "buyer_country": "AUS",
    "seller_name": "Assembly seller71718579",
    "seller_email": "[email protected]",
    "seller_country": "AUS",
    "payment_credit_card_enabled": true,
    "payment_direct_debit_enabled": true,
    "tds_check_state": null,
    "related": {
      "buyers": "buyer-719013950014",
      "sellers": "seller-71718579"
    },
    "links": {
      "self": "/items/7190770-1-2908/make_payment?account_id=725cc8c0-759b-0138-5d6d-0a58a9feac05",
      "buyers": "/items/7190770-1-2908/buyers",
      "sellers": "/items/7190770-1-2908/sellers",
      "status": "/items/7190770-1-2908/status",
      "fees": "/items/7190770-1-2908/fees",
      "transactions": "/items/7190770-1-2908/transactions",
      "batch_transactions": "/items/7190770-1-2908/batch_transactions",
      "wire_details": "/items/7190770-1-2908/wire_details",
      "bpay_details": "/items/7190770-1-2908/bpay_details",
      "tds_checks": "/items/7190770-1-2908/tds_checks"
    }
  }

Key Response Fields:

  • payment_id: Unique identifier for polling payment status
  • payment_token: Security token for 3DS2 SDK authentication

JavaScript SDK

Configuration

Include the 3DS2 JavaScript component

<script src="https://hosted-fields.assemblypay.com/assembly-3ds2.js"></script>

Initialize the 3DS2 component with these parameters:

(new assembly3DS2()).initialize({
    authToken: "payment_token",    // Required: JWT from make_payment_async
    paymentId: "payment_id",              // Required: Payment ID from API response
    env: "pre-live",                      // Required: "pre-live" or "prod"
    onComplete: function() { },           // Required: Completed callback (does not mean payment was successful, only that the 3DS2 flow has reached the end, check item status for final outcome)
     onError: function(error) { },        // Required: Error callback (component encountered an error or challenge was not completed, check item status for final outcome). See `error.message` for more information.
		targetElementId: 'my-container-id'    // Required: Where to insert iframes (eg, such as challenge)
});

Event Callbacks

onComplete()

Called when the 3DS2 flow has been completed. If no challenge was required by card issuer, this will happen without anything being presented to the user. If a challenge was required, this will be called after the challenge for has been submitted.

Note: This does not necessarily indicate that the payment was successful. It is possible that the payment could still be rejected for a number of reasons (eg, insufficient funds). Please check the item status via the API to determine the final payment outcome.

onComplete: (result) => {
  // get final item status via your own API to determine if payment was successful or not
  fetch(`https://<customer-backend>/payments/<customer-payment-id>`, ...)
	    .then(...)
}

onError(error)

Called when an error occurs during the 3DS2 flow. See error.message for a description of the error.

This could happen any many different points in the flow, such as:

  • Network error contacting Zai's servers (eg, if user is having internet connectivity issues)
  • Failure to complete device data collection
  • Failure to load or submit the issuer's challenge form
  • Challenge form not completed successfully
  • Challenge not completed in a timely manner (approx 2 minutes of inactivity).

Note: This does not necessarily indicate that the payment was rejected. It is possible that the payment could have succeed in the background, but the front-end component was unable to contact the server (eg, network connectivity issues). Please check the item status via the API to determine the final payment outcome before attempting the payment again.

onError: (error) => {
    console.error('3DS2 error:', error.message);
    
    // Track error for analytics
    analytics.track('3ds2_error', {
        code: error.code,
        payment_id: paymentId,
        item_id: itemId
    });
    
  // get final item status via your own API to determine if payment was successful or not
  fetch(`https://<customer-backend>/payments/<customer-payment-id>`, ...)
	    .then(...)
}

Authentication Flow

Our JavaScript component handles each step of the 3DS2 process for you

1. Device Data Collection (DDC)

The SDK automatically collects device information in a hidden iframe. This data helps the issuer determine transaction risk without user interaction.

2. Authentication Decision

Three possible outcomes:

Frictionless (No Challenge)

  • Low-risk transaction
  • Issuer approves without user interaction
  • Payment proceeds immediately

Challenge Required

  • Medium/high-risk transaction
  • User must complete additional verification
  • OTP, password, biometric, or knowledge-based auth

Failed Authentication

  • High-risk or invalid transaction
  • Payment declined by issuer
  • User receives error message

3. Challenge Flow (if required)

When a challenge is triggered:

  1. Challenge iframe appears with issuer-controlled interface
  2. User completes authentication (OTP, password, biometrics)
  3. Result processed and authentication status returned
  4. Payment continues based on authentication outcome

4. Payment Completion

After successful authentication:

  1. onComplete callback triggered with result
  2. Item status transitions to appropriate state
  3. Webhook notifications sent (if configured)
  4. Funds captured from card account

Webhooks

For comprehensive webhook setup and configuration, see our Webhooks Guide.

Configure webhooks to receive real-time payment status updates for both item completion and transaction authorization events.



Testing

Sandbox Environment

Use the pre-live environment for testing with realistic scenarios:

Base URLs:

  • API: https://test.api.promisepay.com
  • Auth: https://au-0000.sandbox.auth.assemblypay.com
  • SDK: env: "pre-live"

Test Cards

Use only these test card numbers to simulate different 3DS2 scenarios:

Test Cards

Use these test card numbers to simulate different 3DS2 scenarios:

1. Successful Frictionless Authentication

Card NetworkCard Number
Visa4000000000002701
Mastercard5200000000002235
American Express340000000002708

2. Failed Frictionless Authentication

Card NetworkCard Number
Visa4000000000002925
Mastercard5200000000002276
American Express340000000002096

3. Attempts Stand-In Authentication

Card NetworkCard Number
Visa4000000000002719
Mastercard5200000000002482
American Express340000000002872

4. Unavailable Authentication

Card NetworkCard Number
Visa4000000000002313
Mastercard5200000000002268
American Express340000000002922

5. Rejected Authentication

Card NetworkCard Number
Visa4000000000002537
Mastercard5200000000002185
American Express340000000002062

6. Authentication Not Available on Lookup

Card NetworkCard Number
Visa4000000000002990
Mastercard5200000000002409
American Express340000000002468

7. Error on Lookup

Card NetworkCard Number
Visa4000000000002446
Mastercard5200000000002037
American Express340000000002732

8. Timeout on Lookup

Card NetworkCard Number
Visa4000000000002354
Mastercard5200000000002326
American Express340000000002047

Test Scenarios

Automatic 3DS2 Decision:

curl -X PATCH https://test.api.promisepay.com/items/item_789/make_payment_async \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "card_account_id",
    "request_three_d_secure": "automatic"
  }'

Force Challenge Flow:

curl -X PATCH https://test.api.promisepay.com/items/item_789/make_payment_async \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "card_account_id", 
    "request_three_d_secure": "challenge"
  }'

Allow Any Authentication:

curl -X PATCH https://test.api.promisepay.com/items/item_789/make_payment_async \
  -H "Authorization: Bearer {oauth_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "card_account_id",
    "request_three_d_secure": "any"
  }'

Testing Workflow

  1. Create test users and items using sandbox API
  2. Use test card numbers to simulate different outcomes
  3. Test all authentication modes (automatic, challenge, any)
  4. Verify webhook delivery and payload structure
  5. Test error scenarios and recovery flows
  6. Validate UI behavior across different devices/browsers

Security Considerations

Content Security Policy (CSP)

Ensure the following origins are included in your Content-Security-Policy header:

Content-Security-Policy: 
  frame-src https://*.assemblypay.com https://*.cardinalcommerce.com;
  script-src https://*.assemblypay.com;
  connect-src https://*.api.assemblypay.com;

PCI Compliance

The 3DS2 SDK maintains PCI DSS compliance:

  • No sensitive data storage in your application
  • Secure iframe isolation for card data entry
  • Encrypted data transmission using TLS 1.2+
  • Token-based authentication for API access

Best Practices

  1. Validate payment_token before initializing SDK
  2. Implement token refresh for OAuth tokens
  3. Use HTTPS for all API communications
  4. Whitelist iframe domains in CSP
  5. Monitor for unusual authentication patterns
  6. Log security events for audit purposes

Migration Guide

Choosing the Right Endpoint

Use CaseEndpointWhen to Use
Simple card paymentsmake_paymentNo 3DS2 required, immediate response needed
3DS2 authenticationmake_payment_asyncSCA compliance required, challenge flows
Mixed payment flowsBothRoute based on transaction risk/amount

From Synchronous to Async Payments

If you're currently using the synchronous make_payment endpoint:

Before (Synchronous - No 3DS2):

// Traditional Zai card payment flow
async function processPayment(itemId, cardAccountId) {
    try {
        const response = await fetch(`/items/${itemId}/make_payment`, {
            method: 'PATCH',
            headers: {
                'Authorization': 'Bearer ' + oauthToken,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ 
                account_id: cardAccountId,
                cvv: '123'
            })
        });
        
        const result = await response.json();
        
        // Payment completed immediately
        if (result.items.state === 'completed') {
            window.location.href = '/success';
        } else if (result.items.state === 'payment_held') {
            showMessage('Payment is being processed');
        }
    } catch (error) {
        console.error('Payment failed:', error);
    }
}

After (Asynchronous with 3DS2):

// New 3DS2-enabled payment flow
async function processPaymentWith3DS2(itemId, cardAccountId) {
    try {
        // Step 1: Initiate async payment
        const response = await fetch(`/items/${itemId}/make_payment_async`, {
            method: 'PATCH',
            headers: {
                'Authorization': 'Bearer ' + oauthToken,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                account_id: cardAccountId,
                request_three_d_secure: 'automatic'
            })
        });
        
        const paymentData = await response.json();
        
        // Step 2: Initialize 3DS2 authentication
        assembly3DS2.init({
            authToken: paymentData.payment_token,
            paymentId: paymentData.payment_id,
            env: getEnvironment(), // "pre-live" or "prod"
            onComplete: (result) => {
                if (result.status === "PAYMENT_FINALISED") {
                    window.location.href = '/success';
                }
            },
            onError: (error) => {
                console.error('3DS2 authentication failed:', error);
                showErrorMessage(error.message);
            }
        });
        
    } catch (error) {
        console.error('Payment initiation failed:', error);
    }
}

function getEnvironment() {
    return window.location.hostname.includes('localhost') || 
           window.location.hostname.includes('staging') 
           ? 'pre-live' : 'prod';
}

Production Checklist

Before going live with 3DS2, ensure:

Technical Setup

  • 3DS2 is enabled on your production Zai account
  • OAuth2 authentication implemented and tested
  • Webhooks configured for items and transactions
  • Environment changed from "pre-live" to "prod" in SDK
  • Production API credentials configured
  • CSP and CORS configured correctly

Testing & Validation

  • All test scenarios validated in sandbox
  • Error handling implemented for all scenarios
  • Webhook endpoint tested and secured
  • UI/UX optimized for challenge flows
  • Mobile browser compatibility verified
  • Timeout handling tested

Monitoring & Operations

  • Logging implemented for debugging
  • Error tracking and alerting configured
  • Payment success/failure rates monitored
  • Customer support processes updated
  • Documentation and runbooks created

Security & Compliance

  • PCI DSS requirements reviewed
  • Security headers configured (CSP, HTTPS)
  • Data retention policies defined
  • Incident response procedures documented

Support

Getting Help

Common Issues

Q: The iframe doesn't appear
A: Check your CSP settings and ensure frame-src includes https://*.hellozai.com and issuer domains

Q: Authentication always times out
A: Verify your payment_token is valid and hasn't expired. Check network connectivity and firewall settings.

Q: Challenge flow doesn't work on mobile
A: Ensure your viewport meta tag allows iframe scaling: <meta name="viewport" content="width=device-width, initial-scale=1">

Q: Can I customize the iframe appearance?
A: The iframe content is controlled by the card issuer and cannot be customized for security and compliance reasons.

Q: How do I handle multiple concurrent payments?
A: Each payment requires a unique payment_id and payment_token. Initialize separate SDK instances for concurrent payments.

Q: What happens if the user refreshes during authentication?
A: The payment will timeout. Implement page refresh detection and guide users to restart the payment process.


Last updated: August 19, 2025_

For the most current API documentation, visit developer.hellozai.com_