- Which approach should I use?
- Overview
- How It Works
- Flow Diagram
- Prerequisites
- Step 1: Create and Process a Payment (Single Call)
- Step 2: Handle Bank Authentication (If Required)
- Step 3: Handle Webhook Notifications
- Complete Integration Example
- Testing Your Integration
- Common Integration Patterns
- Security Best Practices
- Troubleshooting
- Support
- Related Documentation
Which approach should I use? #
Use this API‑only guide when you need full control and cannot (or prefer not to) load our JS SDK. For most web integrations, the JS SDK guide is the recommended, faster path.
– Prefer the JS SDK (recommended for most merchants):
– Simplest integration: the SDK collects browser data for you and opens our hosted bank authentication page.
– Built‑in UI: lightbox, iframe, or redirect with safe defaults and automatic return handling.
– Fewer moving parts on your side: one `createPayment` callback, then `paymentComplete`.
– Same server‑side guarantees: payments are finalised server‑to‑server; webhooks remain the source of truth.
– Use this API‑only approach when:
– You cannot include third‑party scripts for policy/CSP reasons.
– You want to orchestrate the UI entirely yourself (kiosks, embedded apps, custom layouts).
– You have a non‑browser client, or a server‑triggered flow that still needs card input you collect.
– You need deep control over when/where the hosted authentication page is shown.
Overview #
Server-to-server card payments allow you to collect card details on your own system and securely process payments through the TrustistEcommerce API. This integration is suitable for merchants who:
– Have their own card collection forms
– Store customer card details in their own systems
– Need more control over the payment user experience
– Are PCI compliant (PCI SAQ-D)
Important: This integration requires that your systems are PCI compliant. If you are not PCI compliant, please use our standard payment link integration instead.
How It Works #
The server-to-server payment flow consists of these steps:
1. Frontend: Collect card details (your own form)
2. Backend: Call the TrustistEcommerce API once with card + browser data
3. Frontend: If needed, open the bank authentication URL we return (iframe or popup)
4. Backend: Receive webhook confirmation of payment completion (source of truth)
Flow Diagram #

The diagram shows two scenarios:
– Scenario A: Frictionless payment – card is approved immediately without 3D Secure
– Scenario B: 3D Secure challenge required – customer authenticates with their bank via PayerServices
Prerequisites #
Before you begin, ensure you have:
1. API Credentials: Your TrustistEcommerce API Key and Client ID
– See Getting Your API Keys
2. Authentication Setup: Understanding of HMAC authentication
– See Authentication Guide
3. Webhook Endpoint: A secure endpoint to receive payment notifications
– See Merchant Webhooks
4. PCI Compliance: Your system must be PCI SAQ-D compliant to handle raw card data
Step 1: Create and Process a Payment (Single Call) #
From your backend, make a single API call that both creates and processes the payment. This request includes the card details and browser data required for authentication.
API Endpoint #
POST https://api-sandbox.trustistecommerce.com/v1/payments/server-to-server
(For production, use: `https://api.trustistecommerce.com/v1/payments/server-to-server`)
Authentication #
Use HMAC authentication as described in our [Authentication Guide](https://trustisttransfer.com/docs/authentication/).
Request Body #
{
"amount": 99.99,
"reference": "ORDER-12345",
"description": "Order for customer John Smith",
"customerDetails": "John Smith, john@example.com",
"payerEmail": "john@example.com",
"cardDetails": {
"number": "4444333322221111",
"expiryMonth": "12",
"expiryYear": "2025",
"cvc": "123"
},
"billingAddress": {
"firstName": "John",
"lastName": "Smith",
"lineOne": "123 High Street",
"lineTwo": "Apartment 4B",
"city": "London",
"region": "Greater London",
"country": "GB",
"postalCode": "SW1A 1AA"
},
"browserData": {
"browserJavaEnabled": false,
"browserJavascriptEnabled": true,
"browserLanguage": "en-GB",
"browserColorDepth": 24,
"browserScreenHeight": 1080,
"browserScreenWidth": 1920,
"browserTZ": -60,
"browserUserAgent": "Mozilla/5.0...",
"browserAcceptHeader": "text/html,application/xhtml+xml...",
"browserIP": "203.0.113.42",
"challengeWindowSize": "FULLSCREEN",
"deviceChannel": "Browser"
}
}
Field Descriptions #
Field | Type | Required | Description |
---|---|---|---|
`amount` | decimal | Yes | Payment amount in GBP (e.g., 99.99) |
`reference` | string | Yes | Your unique order/transaction reference |
`description` | string | No | Payment description |
`customerDetails` | string | No | Customer information for your records |
`payerEmail` | string | Yes | Customer’s email address |
`cardDetails` | object | Yes | Card information (see below) |
`billingAddress` | object | No | Billing address (recommended for 3DS) |
`browserData` | object | Yes | Browser information for 3D Secure |
Card Details Object #
Field | Type | Required | Description |
---|---|---|---|
`number` | string | Yes | Card number (13-19 digits, no spaces) |
`expiryMonth` | string | Yes | Expiry month (MM format, e.g., “12”) |
`expiryYear` | string | Yes | Expiry year (YYYY format, e.g., “2025”) |
`cvc` | string | Yes | Card security code (3-4 digits) |
Browser Data Object #
These fields are required for 3D Secure authentication and can be collected using JavaScript:
Field | Type | JavaScript Code |
---|---|---|
`browserJavaEnabled` | boolean | `navigator.javaEnabled()` |
`browserJavascriptEnabled` | boolean | Always `true` (if JS is running) |
`browserLanguage` | string | `navigator.language` |
`browserColorDepth` | number | `screen.colorDepth` |
`browserScreenHeight` | number | `screen.height` |
`browserScreenWidth` | number | `screen.width` |
`browserTZ` | number | `new Date().getTimezoneOffset()` |
`browserUserAgent` | string | `navigator.userAgent` |
`browserAcceptHeader` | string | From server-side request headers |
`browserIP` | string | Customer’s IP address (server-side) |
`challengeWindowSize` | string | “FULLSCREEN”, “500×600”, etc. |
`deviceChannel` | string | “Browser” or “Application” |
JavaScript Example: Collecting Browser Data #
function collectBrowserData() {
return {
browserJavaEnabled: navigator.javaEnabled(),
browserJavascriptEnabled: true,
browserLanguage: navigator.language,
browserColorDepth: screen.colorDepth,
browserScreenHeight: screen.height,
browserScreenWidth: screen.width,
browserTZ: new Date().getTimezoneOffset(),
browserUserAgent: navigator.userAgent,
challengeWindowSize: "FULLSCREEN",
deviceChannel: "Browser"
};
}
Response #
The API will respond with one of two scenarios:
Scenario 1: Payment Approved (Frictionless) #
If the payment is approved without requiring 3D Secure:
{
"id": "3PAYMENT123456789ABCD",
"status": "COMPLETE",
"requiredAction": null
}
✅ Payment is complete! Proceed to show your success page.
Scenario 2: 3D Secure Required #
If 3D Secure authentication is required:
{
"id": "3PAYMENT123456789ABCD",
"status": "PENDING_ACTION",
"requiredAction": {
"type": "Challenge",
"url": "https://payerservices-sandbox.trustisttransfer.com/ServerToServer/Challenge?paymentId=3PAYMENT123456789ABCD&merchantOrigin=https://yoursite.com"
}
}
⚠️ 3D Secure required! Proceed to Step 2.
Error Responses #
If validation fails, you’ll receive a 400 Bad Request with a descriptive error:
{
"error": "Invalid card number length or format"
}
{
"error": "Card expiry date is in the past"
}
{
"error": "American Express cards are not accepted by this merchant"
}
Step 2: Handle Bank Authentication (If Required) #
If `requiredAction` is present in the response, open the provided authentication URL in an iframe or popup. We host this page and coordinate the bank challenge; no additional SDKs are required.
Option A: Using an iframe (Recommended) #
Open the `requiredAction.url` in an iframe or popup:
async function handle3DSecure(requiredAction) {
if (!requiredAction || requiredAction.type !== "Challenge") {
return; // No challenge required
}
// Open the challenge URL in an iframe
const iframe = document.createElement('iframe');
iframe.src = requiredAction.url;
iframe.style.width = '100%';
iframe.style.height = '600px';
iframe.style.border = 'none';
document.getElementById('payment-container').appendChild(iframe);
// Listen for completion message
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'PAYMENT_COMPLETE') {
// Payment completed after 3DS
const paymentId = event.data.paymentId;
const status = event.data.status;
if (status === 'COMPLETE') {
// Success! Redirect to your success page
window.location.href = '/order-confirmation?payment=' + paymentId;
} else {
// Payment failed
showError('Payment was not successful. Please try again.');
}
}
});
}
Option B: Using a Popup Window #
function handle3DSecurePopup(requiredAction) {
if (!requiredAction || requiredAction.type !== "Challenge") {
return;
}
// Open challenge URL in popup
const popup = window.open(
requiredAction.url,
'3DSecureChallenge',
'width=500,height=600,scrollbars=yes'
);
// Listen for completion
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'PAYMENT_COMPLETE') {
popup.close();
if (event.data.status === 'COMPLETE') {
window.location.href = '/order-confirmation?payment=' + event.data.paymentId;
} else {
showError('Payment failed after authentication.');
}
}
});
}
What Happens During Authentication? #
1. The customer is presented with their bank’s authentication challenge (e.g., enter a code sent via SMS)
2. After completing the challenge, we automatically process and capture the payment if approved
3. We post a message back to your page with the final payment status
4. You can then redirect the customer to your success or failure page
Step 3: Handle Webhook Notifications #
While the frontend receives immediate feedback, you should always rely on webhook notifications for the authoritative payment status.
Why Webhooks? #
– Customer may close browser before frontend completes
– Network issues may prevent frontend from receiving status
– Webhooks provide server-to-server confirmation
Webhook Events #
You’ll receive one of these events:
Event | Description |
---|---|
`payment.completed` | Payment successfully captured |
`payment.failed` | Payment failed or declined |
Webhook Payload Example #
{
"eventType": "payment.completed",
"paymentId": "3PAYMENT123456789ABCD",
"merchantId": "your_merchant_id",
"amount": 99.99,
"reference": "ORDER-12345",
"status": "COMPLETE",
"timestamp": "2025-10-01T14:30:00Z"
}
Handling Webhooks #
See our Merchant Webhooks Guide for:
– Webhook authentication
– Idempotency handling
– Retry logic
– Testing webhooks
Complete Integration Example #
Here’s a complete example showing all steps together:
Backend Code (Language-Agnostic Pseudocode) #
// 1. Create and process payment with card details (single call)
function processPayment(orderData, cardDetails, browserData):
// Build payment request
paymentRequest = {
amount: orderData.total,
reference: orderData.orderId,
description: orderData.description,
payerEmail: orderData.customerEmail,
cardDetails: cardDetails,
billingAddress: orderData.billingAddress,
browserData: browserData
}
// Call TrustistEcommerce API with HMAC authentication (single call)
response = httpPost(
url: "https://api.trustistecommerce.com/v1/payments/server-to-server",
body: paymentRequest,
headers: {
"Authorization": generateHmacAuth(paymentRequest),
"Content-Type": "application/json"
}
)
if response.status == 400:
// Validation error
return {
success: false,
error: response.body.error
}
if response.body.status == "COMPLETE":
// Frictionless success
return {
success: true,
paymentId: response.body.id,
completed: true
}
else if response.body.requiredAction:
// 3DS required
return {
success: true,
paymentId: response.body.id,
completed: false,
requiredAction: response.body.requiredAction
}
return {
success: false,
error: "Unexpected response"
}
// 2. Webhook handler
function handleWebhook(webhookPayload, webhookSignature):
// Verify webhook signature (see Webhook docs)
if not verifyWebhookSignature(webhookPayload, webhookSignature):
return 401 // Unauthorized
// Process webhook
if webhookPayload.eventType == "payment.completed":
// Mark order as paid in your database
updateOrderStatus(webhookPayload.reference, "PAID")
sendConfirmationEmail(webhookPayload.paymentId)
else if webhookPayload.eventType == "payment.failed":
// Mark order as failed
updateOrderStatus(webhookPayload.reference, "FAILED")
sendFailureNotification(webhookPayload.paymentId)
return 200 // OK
Frontend Code (JavaScript) #
<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
<!-- No SDK required for bank authentication -->
</head>
<body>
<form id="payment-form">
<label>
Card Number:
<input type="text" id="card-number" maxlength="19" />
</label>
<label>
Expiry (MM/YY):
<input type="text" id="expiry" placeholder="12/25" />
</label>
<label>
CVC:
<input type="text" id="cvc" maxlength="4" />
</label>
<button type="submit">Pay £99.99</button>
</form>
<div id="payment-container"></div>
<div id="error-message" style="display:none; color:red;"></div>
<script>
// Collect browser data
function collectBrowserData() {
return {
browserJavaEnabled: navigator.javaEnabled(),
browserJavascriptEnabled: true,
browserLanguage: navigator.language,
browserColorDepth: screen.colorDepth,
browserScreenHeight: screen.height,
browserScreenWidth: screen.width,
browserTZ: new Date().getTimezoneOffset(),
browserUserAgent: navigator.userAgent,
challengeWindowSize: "FULLSCREEN",
deviceChannel: "Browser"
};
}
// Handle form submission
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
// Get card details
const cardNumber = document.getElementById('card-number').value.replace(/\s/g, '');
const expiry = document.getElementById('expiry').value.split('/');
const cvc = document.getElementById('cvc').value;
// Build payment request
const paymentData = {
amount: 99.99,
reference: 'ORDER-12345',
payerEmail: 'customer@example.com',
cardDetails: {
number: cardNumber,
expiryMonth: expiry[0],
expiryYear: '20' + expiry[1],
cvc: cvc
},
browserData: collectBrowserData()
};
try {
// Call your backend
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(paymentData)
});
const result = await response.json();
if (!result.success) {
// Show error
document.getElementById('error-message').textContent = result.error;
document.getElementById('error-message').style.display = 'block';
return;
}
if (result.completed) {
// Frictionless - payment complete!
window.location.href = '/order-confirmation?payment=' + result.paymentId;
} else if (result.requiredAction) {
// Handle 3D Secure
handle3DSecure(result.requiredAction);
}
} catch (error) {
document.getElementById('error-message').textContent = 'Payment processing failed. Please try again.';
document.getElementById('error-message').style.display = 'block';
}
});
// Handle 3D Secure challenge
function handle3DSecure(requiredAction) {
const iframe = document.createElement('iframe');
iframe.src = requiredAction.url;
iframe.style.width = '100%';
iframe.style.height = '600px';
iframe.style.border = 'none';
document.getElementById('payment-container').appendChild(iframe);
document.getElementById('payment-form').style.display = 'none';
// Listen for completion
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'PAYMENT_COMPLETE') {
if (event.data.status === 'COMPLETE') {
window.location.href = '/order-confirmation?payment=' + event.data.paymentId;
} else {
document.getElementById('error-message').textContent = 'Payment was not successful.';
document.getElementById('error-message').style.display = 'block';
document.getElementById('payment-form').style.display = 'block';
iframe.remove();
}
}
});
}
</script>
</body>
</html>
Testing Your Integration #
Sandbox Environment #
Use these endpoints for testing:
– API: `https://api-sandbox.trustistecommerce.com`
(We return the correct hosted authentication URL when needed.)
Test Card Numbers #
3D-secure test cards #
Scheme | Card Number | 3DS flow | Description |
---|---|---|---|
Visa | 4012000000060085 | Frictionless | Authentication approved immediately |
Visa | 4066330000000004 | 3DS method frictionless | Authentication approved after DDC step complete |
Visa | 4938730000000001 | 3DS method challenge | DDC step followed by challenge |
Visa | 4918190000000002 | 3DS challenge | Immediately challenged |
Mastercard | 5555555555554444 | Frictionless | Authentication approved immediately |
Mastercard | 5454545454545454 | 3DS method frictionless | Authentication approved after DDC step complete |
Mastercard | 5200000000001021 | 3DS method challenge | DDC step followed by challenge |
Mastercard | 5295795976859395 | 3DS challenge | Immediately challenged |
Amex | 343434343434343 | Frictionless | Authentication approved immediately |
Amex | 371449635398431 | 3DS method frictionless | Authentication approved after DDC step complete |
Amex | 374245455400126 | 3DS method challenge | DDC step followed by challenge |
Amex | 374101000000608 | 3DS challenge | Immediately challenged |
Declined Payments #
Scheme | Card Number | CVV | Description |
---|---|---|---|
Visa | 4000000000001000 | 100 | Declined due to insufficient funds |
Mastercard | 5425233430109903 | 100 | Declined due to insufficient funds |
Visa | 4000000000001000 | 001 | Declined due to incorrect CVV |
Mastercard | 5425233430109903 | 001 | Declined due to incorrect CVV |
Visa | 4000000000001000 | 222 | Declined due to issuer reason “Do not honour” |
Mastercard | 5425233430109903 | 222 | Declined due to issuer reason “Do not honour” |
Amex | 378282246310005 | 1000 | Declined due to insufficient funds |
Testing Checklist #
– [ ] Frictionless payment completes successfully
– [ ] 3DS challenge displays correctly in iframe/popup
– [ ] Payment completes after 3DS authentication
– [ ] Error messages display for invalid card numbers
– [ ] Error messages display for expired cards
– [ ] Webhook endpoint receives `payment.completed` event
– [ ] Webhook endpoint receives `payment.failed` event
– [ ] Order status updates correctly from webhooks
Common Integration Patterns #
Pattern 1: Stored Cards #
If you store customer cards in your own PCI-compliant system:
// Retrieve stored card
storedCard = getStoredCard(customerId, cardId)
// Process payment with stored card
result = processPayment(orderData, storedCard, browserData)
Pattern 2: Guest Checkout #
For one-time payments without customer accounts:
// Collect card on checkout page
cardDetails = collectCardFromForm()
// Process immediately
result = processPayment(orderData, cardDetails, browserData)
Pattern 3: Retry Failed Payments #
If a payment fails, allow the customer to try a different card:
if paymentResult.status == "FAILED":
// Show error and allow new card entry
showError("Payment declined. Please try a different card.")
resetPaymentForm()
Security Best Practices #
1. Never log or store raw card details on your servers
2. Use HTTPS for all communication
3. Validate card details on the frontend before submission
4. Implement rate limiting to prevent abuse
5. Verify webhook signatures to prevent spoofing
6. Use idempotency keys to prevent duplicate charges
7. Sanitize all input to prevent injection attacks
Troubleshooting #
Payment Returns “Uncaught exception” #
– Check that all required fields are provided
– Verify card number length (13-19 digits)
– Ensure expiry date is in the future
– Check CVC length (3-4 digits)
Authentication iframe doesn’t load #
– Verify the `requiredAction.url` is not blocked by CSP
– Check browser console for errors
(No SDK is required.)
Webhook not received #
– Verify webhook URL is publicly accessible
– Check webhook signature verification
– Review webhook logs in TrustistEcommerce portal
– Test webhook endpoint using Postman
Payment status stays “PROCESSING” #
– Wait for webhook notification (may take a few seconds)
(If you see repeated processing, contact support.)
– Contact support if status doesn’t update within 2 minutes
Support #
For additional help:
– Documentation: https://trustisttransfer.com/docs/
– Email: info@trustist.com
– API Status: Check the Trustist status page for any service issues