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:
- ✅ Zai Account with 3DS2 enabled - Contact Support
- 🔑 OAuth2 Authentication implemented - See Authentication Guide
- 📡 Webhooks configured - See Webhook Guide
- 👤 Users and Wallets created in Zai
- 💳 Card Accounts tokenised and linked to users
- 🧪 Sandbox Access via dashboard.hellozai.com
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:
| Feature | make_payment | make_payment_async |
|---|---|---|
| Flow Type | Synchronous | Asynchronous |
| 3DS2 Support | ❌ No | ✅ Yes |
| Response | Final payment result | Payment acknowledgment + status token |
| Use Case | Simple card payments | 3DS2 authentication required |
| Integration | Direct API response | Requires 3DS2 SDK + polling |
| Environment | All | Pre-live |
non 3DS2 Payments
For non-3DS2 transactions, use the traditional make_payment endpoint:
PATCH /items/{item_id}/make_paymentParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id | string | ✅ | Card account ID for payment |
device_id | string | ❌ | Device information for fraud detection |
ip_address | string | ❌ | IP address for fraud detection |
cvv | string | ❌ | CVV verification |
3DS2 Payments
For payments requiring 3DS2 authentication:
PATCH /items/{item_id}/make_payment_asyncParameters:
| Parameter | Type | Required |
|---|---|---|
account_id | string | ✅ |
request_three_d_secure | string | ❌ |
Authentication Modes:
automatic(default): Zai determines if 3DS2 is required based on risk analysischallenge: Request a 3DS2 challenge to be presented to userany: 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 statuspayment_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:
- Challenge iframe appears with issuer-controlled interface
- User completes authentication (OTP, password, biometrics)
- Result processed and authentication status returned
- Payment continues based on authentication outcome
4. Payment Completion
After successful authentication:
onCompletecallback triggered with result- Item status transitions to appropriate state
- Webhook notifications sent (if configured)
- 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 Network | Card Number |
|---|---|
| Visa | 4000000000002701 |
| Mastercard | 5200000000002235 |
| American Express | 340000000002708 |
2. Failed Frictionless Authentication
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002925 |
| Mastercard | 5200000000002276 |
| American Express | 340000000002096 |
3. Attempts Stand-In Authentication
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002719 |
| Mastercard | 5200000000002482 |
| American Express | 340000000002872 |
4. Unavailable Authentication
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002313 |
| Mastercard | 5200000000002268 |
| American Express | 340000000002922 |
5. Rejected Authentication
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002537 |
| Mastercard | 5200000000002185 |
| American Express | 340000000002062 |
6. Authentication Not Available on Lookup
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002990 |
| Mastercard | 5200000000002409 |
| American Express | 340000000002468 |
7. Error on Lookup
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002446 |
| Mastercard | 5200000000002037 |
| American Express | 340000000002732 |
8. Timeout on Lookup
| Card Network | Card Number |
|---|---|
| Visa | 4000000000002354 |
| Mastercard | 5200000000002326 |
| American Express | 340000000002047 |
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
- Create test users and items using sandbox API
- Use test card numbers to simulate different outcomes
- Test all authentication modes (automatic, challenge, any)
- Verify webhook delivery and payload structure
- Test error scenarios and recovery flows
- 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
- Validate payment_token before initializing SDK
- Implement token refresh for OAuth tokens
- Use HTTPS for all API communications
- Whitelist iframe domains in CSP
- Monitor for unusual authentication patterns
- Log security events for audit purposes
Migration Guide
Choosing the Right Endpoint
| Use Case | Endpoint | When to Use |
|---|---|---|
| Simple card payments | make_payment | No 3DS2 required, immediate response needed |
| 3DS2 authentication | make_payment_async | SCA compliance required, challenge flows |
| Mixed payment flows | Both | Route 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
- Documentation: developer.hellozai.com
- Support Email: [email protected]
- Dashboard: dashboard.hellozai.com
- Status Page: Monitor API status and incidents
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_
Updated 2 days ago
