How It Works
Server-to-Server integration gives you full control over the payment flow. There are five distinct phases for Apple Pay. Google Pay follows a similar pattern with simpler token handling.
User taps Apple Pay / Google Pay button
Your server calls PAYSTRAX to validate merchant (Apple Pay) or receives Google Pay token
Payment sheet appears, user authorizes with Face ID / Touch ID / biometrics
Your server decrypts the EC_v1 token (Apple Pay) or extracts Google Pay token
POST decrypted card data to PAYSTRAX API – payment processed
Prerequisites
- PCI-DSS compliance is required. Server-to-Server means your server receives the decrypted card number (DPAN), expiry, and cryptogram, placing it in PCI-DSS scope. If you cannot hold card data, use the hosted/widget integration instead.
- PAYSTRAX credentials: your
entityIdand access token (from your PAYSTRAX dashboard). - Channel configuration: the
APPLEPAYand/orGOOGLEPAYbrands enabled, and CVV optional enabled (wallet tokens carry no CVV). Contact support@paystrax.com if unsure. - Webhook secret: the 64-character hex secret from your dashboard (needed to decrypt webhooks – see Webhooks below).
Apple Pay – Choose Your Setup
There are two ways to integrate Apple Pay S2S. Both produce identical checkout experiences – the difference is who manages the Apple Merchant ID.
Option A – PAYSTRAX Merchant ID
No Apple Developer account needed. PAYSTRAX registers your domain under our Apple Merchant ID. You generate your own Payment Processing key pair for decryption.
Get startedOption B – Your Own Apple Developer Account
Full independence. You create and manage your own Merchant ID, Payment Processing Certificate, and domain registration in Apple Developer.
Get startedOption A – No Apple Developer Account
Contact PAYSTRAX
Request domain registration and domain verification file
Email support@paystrax.com with your checkout domain (e.g. checkout.yourstore.com). Request:
- The
apple-developer-merchantid-domain-associationverification file - Domain registration under PAYSTRAX’s Apple Merchant ID
PAYSTRAX will send you the file and register your domain. This usually takes less than 1 business day.
Host the Domain Verification File
Place at /.well-known/ on your domain
Host the file so it is publicly accessible at exactly this URL:
https://[your-domain]/.well-known/apple-developer-merchantid-domain-association
<Files "apple-developer-merchantid-domain-association">
ForceType application/octet-stream
</Files>location /.well-known/apple-developer-merchantid-domain-association {
default_type application/octet-stream;
}Generate Payment Processing Key Pair
EC private key + certificate for server-side token decryption
You need an EC key pair so your server can decrypt the Apple Pay token. Generate one using OpenSSL:
# Step 1: Generate EC private key (prime256v1 curve)
openssl ecparam -name prime256v1 -genkey -noout -out merchant_payment.key
# Step 2: Generate CSR (upload this to Apple Developer portal)
openssl req -new -key merchant_payment.key -out merchant_payment.csr \
-subj "/CN=Your Merchant/O=YourCompany/C=IE"
# Step 3: After Apple issues the .cer file, convert to PEM
openssl x509 -inform DER -in merchant_payment.cer -out merchant_payment.pem# Store files securely
/home/youruser/domains/yoursite.com/public_html/private/merchant_payment.key
/home/youruser/domains/yoursite.com/public_html/private/merchant_payment.pem
# Block web access to this directory (.htaccess)
Deny from allImplement the Integration
Frontend + backend code – see sections below
Proceed to Apple Pay Frontend Code, Backend Merchant Validation, Token Decryption, and Payment Submission.
Option B – Your Own Apple Developer Account
Create a Merchant ID
In Apple Developer portal
Go to developer.apple.com – Certificates, Identifiers & Profiles – Identifiers – add a Merchant ID. Use a format like merchant.com.yourcompany.checkout.
Create a Payment Processing Certificate
Generates the key pair used for token decryption
Under your Merchant ID – Apple Pay Payment Processing Certificate – Create Certificate. Generate a CSR and upload it:
# Generate EC private key
openssl ecparam -name prime256v1 -genkey -noout -out merchant_payment.key
# Generate CSR (upload this to Apple Developer)
openssl req -new -key merchant_payment.key -out merchant_payment.csr \
-subj "/CN=merchant.com.yourcompany/O=YourCompany/C=US"
# After Apple issues the cert, convert .cer to PEM
openssl x509 -inform DER -in merchant_payment.cer -out merchant_payment.pemUpload the CSR to Apple, download the issued .cer file, and convert it to PEM. Store both merchant_payment.key and merchant_payment.pem securely on your server.
Register Your Domain
Apple Pay on the Web – Add Domain
Under your Merchant ID – Apple Pay on the Web – Add Domain. Apple provides a domain verification file. Host it at /.well-known/apple-developer-merchantid-domain-association and complete verification.
Implement the Integration
Same code as Option A
The frontend and backend code is identical to Option A. Proceed to the sections below.
Apple Pay Frontend Code
<!-- Apple Pay button CSS - only shown in Safari -->
<style>
#apple-pay-button {
-webkit-appearance: -apple-pay-button;
-apple-pay-button-type: buy;
-apple-pay-button-style: black;
width: 100%;
height: 48px;
border-radius: 8px;
border: none;
cursor: pointer;
display: none;
}
</style>
<button id="apple-pay-button"></button>
<script>
const BASE_URL = 'https://yoursite.com/';
const AMOUNT = '10.00';
const CURRENCY = 'EUR';
let applePayCheckoutId = null;
// Show button only in Safari with Apple Pay available
if (window.ApplePaySession && ApplePaySession.canMakePayments()) {
document.getElementById('apple-pay-button').style.display = 'block';
}
document.getElementById('apple-pay-button').addEventListener('click', () => {
const request = {
countryCode: 'IE',
currencyCode: CURRENCY,
merchantCapabilities: ['supports3DS'],
supportedNetworks: ['visa', 'masterCard', 'amex'],
total: {
label: 'Your Store',
amount: AMOUNT
}
};
const session = new ApplePaySession(14, request);
// -----------------------------------------------------------
// onvalidatemerchant: proxy through your server to PAYSTRAX
// -----------------------------------------------------------
session.onvalidatemerchant = (event) => {
fetch(`${BASE_URL}api/applepay_validate.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ validationURL: event.validationURL })
})
.then(r => r.json())
.then(data => {
if (data.error) {
console.error('Validation failed:', data.error);
session.abort();
return;
}
applePayCheckoutId = data.checkoutId;
session.completeMerchantValidation(data.merchantSession);
})
.catch((err) => {
console.error('Validation error:', err);
session.abort();
});
};
// -----------------------------------------------------------
// onpaymentauthorized: send full token to your server
// -----------------------------------------------------------
session.onpaymentauthorized = (event) => {
const token = event.payment.token;
fetch(`${BASE_URL}api/checkout_payment.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
checkoutId: applePayCheckoutId,
applePayToken: JSON.stringify(token),
amount: AMOUNT,
currency: CURRENCY,
shopperResultUrl: `${BASE_URL}result.html`
})
})
.then(r => r.json())
.then(data => {
const code = data?.result?.code ?? '';
const ok = /^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.[12]0)/.test(code);
session.completePayment(
ok ? ApplePaySession.STATUS_SUCCESS
: ApplePaySession.STATUS_FAILURE
);
if (ok) {
window.location = `${BASE_URL}result.html?id=${data.id}`;
}
})
.catch(() => {
session.completePayment(ApplePaySession.STATUS_FAILURE);
});
};
session.begin();
});
</script>Backend – Merchant Validation
When Apple Pay triggers onvalidatemerchant, your server creates a PAYSTRAX checkout session and proxies the Apple Pay session call. PAYSTRAX uses its Merchant Identity Certificate to authenticate with Apple on your behalf.
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../config.php';
$input = json_decode(file_get_contents('php://input'), true);
$validationURL = $input['validationURL'] ?? null;
if (!$validationURL) {
echo json_encode(['error' => 'Missing validationURL']);
exit;
}
// -------------------------------------------------------
// Step 1: Create PAYSTRAX checkout session
// -------------------------------------------------------
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => S2S_API_URL . '/v1/checkouts',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . S2S_ACCESS_TOKEN],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'entityId' => S2S_ENTITY_ID,
'amount' => '0.01',
'currency' => 'EUR',
'paymentType' => 'DB',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$checkoutResp = json_decode(curl_exec($ch), true);
curl_close($ch);
$checkoutId = $checkoutResp['id'] ?? null;
if (!$checkoutId) {
echo json_encode(['error' => 'Failed to create checkout session',
'detail' => $checkoutResp]);
exit;
}
// -------------------------------------------------------
// Step 2: Call PAYSTRAX Apple Pay session endpoint
// -------------------------------------------------------
$sessionPayload = [
'merchantIdentifier' => APPLE_PAY_DOMAIN,
'displayName' => APPLE_PAY_DISPLAY_NAME,
'initiative' => 'web',
'initiativeContext' => APPLE_PAY_DOMAIN,
'validationURL' => $validationURL,
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => S2S_API_URL . '/v1/checkouts/' . $checkoutId . '/applePaySession',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . S2S_ACCESS_TOKEN,
'Content-Type: application/json'],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($sessionPayload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$sessionResp = curl_exec($ch);
curl_close($ch);
$session = json_decode($sessionResp, true);
if (!$session || isset($session['result'])) {
echo json_encode(['error' => 'Apple Pay session failed',
'detail' => $session]);
exit;
}
echo json_encode([
'merchantSession' => $session,
'checkoutId' => $checkoutId,
]);<?php
// PAYSTRAX API credentials (from your PAYSTRAX dashboard)
define('S2S_ENTITY_ID', 'your_entity_id_here');
define('S2S_ACCESS_TOKEN', 'your_access_token_here');
define('S2S_API_URL', 'https://eu-prod.oppwa.com');
// Apple Pay domain config
define('APPLE_PAY_DOMAIN', 'checkout.yourstore.com');
define('APPLE_PAY_DISPLAY_NAME', 'Your Store');
// Payment Processing Certificate - for server-side token decryption
define('APPLE_PAY_PROCESSING_KEY_PATH',
__DIR__ . '/private/merchant_payment.key');
define('APPLE_PAY_PROCESSING_CERT_PATH',
__DIR__ . '/private/merchant_payment.pem');Server-Side Token Decryption
Apple Pay tokens (EC_v1) are encrypted with AES-256-GCM. The decryption key is derived using ECDH + a specific KDF. The following PHP code implements the exact specification confirmed working in production.
<?php
/**
* Apple Pay EC_v1 Token Decryption
* KDF: SHA256( \x00\x00\x00\x01 || sharedSecret || @@@0@@@ || @@@1@@@ || merchantIdBytes )
* Cipher: AES-256-GCM, IV = 16 zero bytes, auth tag = last 16 bytes of data
*/
function applePayDecryptToCardFields(array $paymentData, string $keyPath, string $certPath): array
{
if (($paymentData['version'] ?? '') !== 'EC_v1') {
throw new \RuntimeException('Only EC_v1 supported');
}
$encryptedRaw = base64_decode($paymentData['data']);
$ephemeralKeyDer = base64_decode($paymentData['header']['ephemeralPublicKey']);
// Load private key and certificate
$privateKey = openssl_pkey_get_private(file_get_contents($keyPath));
if (!$privateKey) {
throw new \RuntimeException('Private key load failed');
}
$certPem = file_get_contents($certPath);
$cert = openssl_x509_read($certPem);
if (!$cert) {
throw new \RuntimeException('Certificate load failed');
}
// Extract merchant ID from OID 1.2.840.113635.100.6.32
$merchantIdHex = extractMerchantIdFromCert($certPem);
if (strlen($merchantIdHex) !== 64) {
throw new \RuntimeException('Merchant ID not found in cert (expected 64 hex chars)');
}
$merchantIdBytes = hex2bin($merchantIdHex);
// Parse ephemeral public key from DER to PEM
$ephemeralPem = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split(base64_encode($ephemeralKeyDer), 64, "\n")
. "-----END PUBLIC KEY-----\n";
$ephemeralKey = openssl_pkey_get_public($ephemeralPem);
if (!$ephemeralKey) {
throw new \RuntimeException('Ephemeral key parse failed');
}
// ECDH: derive shared secret (32 bytes)
$sharedSecret = openssl_pkey_derive($ephemeralKey, $privateKey, 32);
if ($sharedSecret === false) {
throw new \RuntimeException('ECDH key derivation failed');
}
// KDF: NIST SP 800-56A Concat with SHA-256
$symmetricKey = hash('sha256',
"\x00\x00\x00\x01" // counter (fixed = 1)
. $sharedSecret // ECDH shared secret
. "\x0d" . 'id-aes256-GCM' // algorithm ID (0x0D = 13 = strlen)
. 'Apple' // partyU - fixed string per Apple spec
. $merchantIdBytes, // partyV - 32 bytes from cert OID
true // raw 32-byte output
);
// AES-256-GCM: split ciphertext and auth tag
$dataLen = strlen($encryptedRaw);
$ciphertext = substr($encryptedRaw, 0, $dataLen - 16);
$authTag = substr($encryptedRaw, $dataLen - 16);
$iv = str_repeat("\x00", 16);
$decrypted = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$symmetricKey,
OPENSSL_RAW_DATA,
$iv,
$authTag
);
if ($decrypted === false) {
throw new \RuntimeException(
'AES-256-GCM decryption failed: ' . openssl_error_string()
);
}
$payload = json_decode($decrypted, true);
if (!is_array($payload)) {
throw new \RuntimeException('Decrypted data is not valid JSON');
}
// Extract card fields from the decrypted payload
$expiry = $payload['applicationExpirationDate'] ?? ''; // format: YYMMDD
$expiryMonth = substr($expiry, 2, 2);
$expiryYear = '20' . substr($expiry, 0, 2);
$cryptogram = $payload['paymentData']['onlinePaymentCryptogram'] ?? '';
$eci = $payload['paymentData']['eciIndicator'] ?? '07';
$dpan = $payload['applicationPrimaryAccountNumber'] ?? '';
if (!$dpan) {
throw new \RuntimeException('No DPAN in decrypted payload');
}
return [
'paymentBrand' => 'APPLEPAY',
'card.number' => $dpan,
'card.expiryMonth' => $expiryMonth,
'card.expiryYear' => $expiryYear,
'threeDSecure.verificationId' => $cryptogram,
'threeDSecure.eci' => $eci,
'applePay.source' => 'web',
];
}
/**
* Extract merchant identifier (SHA-256 hex) from cert OID 1.2.840.113635.100.6.32
* The cert encodes it as @@@47@@@ + 64 hex chars after the OID TLV bytes.
*/
function extractMerchantIdFromCert(string $certPem): string
{
// Decode cert PEM to raw DER bytes
$certDer = base64_decode(
preg_replace('/-----[^-]+-----|[\r\n\s]/', '', $certPem)
);
// OID 1.2.840.113635.100.6.32 in DER-encoded form
$targetOid = "\x06\x09\x2a\x86\x48\x86\xf7\x63\x64\x06\x20";
$pos = strpos($certDer, $targetOid);
if ($pos === false) {
return '';
}
// Scan past the OID, find the @@@52@@@ marker, read 64 hex chars
$offset = $pos + strlen($targetOid);
$window = substr($certDer, $offset, 40);
$atPos = strpos($window, '@');
if ($atPos === false) {
return '';
}
$hex = substr($certDer, $offset + $atPos + 1, 64);
return (strlen($hex) === 64 && ctype_xdigit($hex)) ? $hex : '';
}Payment Submission
After decryption, POST the card fields to POST /v1/payments. Key points:
- Always send paymentBrand=APPLEPAY – do not send the card network (MASTER, VISA)
- Send the cryptogram as threeDSecure.verificationId, NOT as card.cvv
- Do NOT send card.holder – Apple Pay tokens do not contain cardholder name
- Your PAYSTRAX entity must have CVV optional enabled – contact PAYSTRAX if you get error 100.100.600
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/applepay_decrypt.php';
$input = json_decode(file_get_contents('php://input'), true);
$applePayToken = $input['applePayToken'] ?? null;
$amount = number_format((float)($input['amount'] ?? 0), 2, '.', '');
$currency = strtoupper($input['currency'] ?? 'EUR');
// Parse token: frontend sends the full token object
// (paymentData + paymentMethod + transactionIdentifier)
$fullToken = json_decode($applePayToken, true);
$tokenData = $fullToken['paymentData'] ?? $fullToken;
// Decrypt using server-side key + cert
try {
$cardFields = applePayDecryptToCardFields(
$tokenData,
APPLE_PAY_PROCESSING_KEY_PATH,
APPLE_PAY_PROCESSING_CERT_PATH
);
} catch (\Throwable $e) {
echo json_encode(['error' => 'Decryption failed',
'detail' => $e->getMessage()]);
exit;
}
// Build payment fields - paymentBrand MUST be APPLEPAY
$fields = array_merge($cardFields, [
'entityId' => S2S_ENTITY_ID,
'amount' => $amount,
'currency' => $currency,
'paymentType' => 'DB',
'shopperResultUrl' => $input['shopperResultUrl'] ?? '',
]);
// POST to PAYSTRAX /v1/payments
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => S2S_API_URL . '/v1/payments',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . S2S_ACCESS_TOKEN],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;What PAYSTRAX Receives (Successful Request)
paymentBrand = APPLEPAY
card.number = 5355580000000548 (DPAN from token)
card.expiryMonth = 02
card.expiryYear = 2029
threeDSecure.verificationId = MGXZs...BFKA= (cryptogram)
threeDSecure.eci = 07
applePay.source = web
entityId = 8ac9a4ca9d6d2134...
amount = 10.00
currency = EUR
paymentType = DBGoogle Pay S2S Integration
Google Pay S2S is simpler than Apple Pay – tokens are gateway-encrypted by Google and passed directly to PAYSTRAX. No server-side decryption is needed on your end.
Load Google Pay JS API
Include the Google Pay library on your checkout page
<script src="https://pay.google.com/gp/p/js/pay.js"></script>
<div id="google-pay-container"></div>Configure PaymentsClient
Set environment and payment method parameters
const GOOGLE_PAY_ENV = 'PRODUCTION'; // @@@1@@@ for testing
const ENTITY_ID = 'YOUR_ENTITY_ID';
const AMOUNT = '10.00';
const CURRENCY = 'EUR';
const baseCardPaymentMethod = {
type: 'CARD',
parameters: {
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
allowedCardNetworks: ['MASTERCARD', 'VISA']
}
};
const tokenizationSpec = {
type: 'PAYMENT_GATEWAY',
parameters: {
gateway: 'aciworldwide',
gatewayMerchantId: ENTITY_ID
}
};
const cardPaymentMethod = Object.assign(
{},
baseCardPaymentMethod,
{ tokenizationSpecification: tokenizationSpec }
);
const paymentsClient = new google.payments.api.PaymentsClient({
environment: GOOGLE_PAY_ENV
});Check Readiness and Show Button
Verify Google Pay is available before displaying
paymentsClient.isReadyToPay({
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [baseCardPaymentMethod]
})
.then(function(response) {
if (response.result) {
const button = paymentsClient.createButton({
onClick: onGooglePayClicked,
buttonColor: 'black',
buttonType: 'buy',
buttonSizeMode: 'fill'
});
document.getElementById('google-pay-container')
.appendChild(button);
}
})
.catch(function(err) {
console.error('Google Pay readiness check failed:', err);
});Handle Payment
Load payment data and extract token
function onGooglePayClicked() {
const paymentDataRequest = {
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [cardPaymentMethod],
transactionInfo: {
totalPriceStatus: 'FINAL',
totalPrice: AMOUNT,
currencyCode: CURRENCY,
countryCode: 'IE'
},
merchantInfo: {
merchantName: 'Your Store',
merchantId: 'YOUR_GOOGLE_MERCHANT_ID'
}
};
paymentsClient.loadPaymentData(paymentDataRequest)
.then(function(paymentData) {
// Extract the gateway-encrypted token
const token = paymentData
.paymentMethodData
.tokenizationData
.token;
// Send to your server
submitGooglePayToServer(token);
})
.catch(function(err) {
console.error('Google Pay payment failed:', err);
});
}Submit to PAYSTRAX
POST the token to /v1/payments on your server
function submitGooglePayToServer(token) {
fetch('/api/googlepay_payment.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
googlePayToken: token,
amount: AMOUNT,
currency: CURRENCY
})
})
.then(r => r.json())
.then(data => {
const code = data?.result?.code ?? '';
const ok = /^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.[12]0)/.test(code); {
window.location = `/result.html?id=${data.id}`;
} else {
alert('Payment failed: ' + (data?.result?.description ?? code));
}
})
.catch(err => console.error('Submit error:', err));
}Server-Side: POST to PAYSTRAX
PHP backend forwards the token directly
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../config.php';
$input = json_decode(file_get_contents('php://input'), true);
$token = $input['googlePayToken'] ?? '';
$amount = number_format((float)($input['amount'] ?? 0), 2, '.', '');
$currency = strtoupper($input['currency'] ?? 'EUR');
if (!$token) {
echo json_encode(['error' => 'Missing Google Pay token']);
exit;
}
// Build payment fields for PAYSTRAX
$fields = [
'entityId' => S2S_ENTITY_ID,
'amount' => $amount,
'currency' => $currency,
'paymentType' => 'DB',
'paymentBrand' => 'GOOGLEPAY',
'googlePay.paymentToken' => $token,
];
// POST to PAYSTRAX /v1/payments
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => S2S_API_URL . '/v1/payments',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . S2S_ACCESS_TOKEN],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;What PAYSTRAX Receives (Google Pay)
paymentBrand = GOOGLEPAY
virtualAccount.paymentToken = {"signature":"MEUCIQDx...","protocolVersion":"ECv2",...}
entityId = 8ac9a4ca9d6d2134...
amount = 10.00
currency = EUR
paymentType = DBResult Codes
Success Codes
| Code | Meaning | Action |
|---|---|---|
| 000.000.000 | Transaction succeeded | Payment complete – show success page |
| 000.100.110 | Request successfully processed in “Merchant in Integrator Test Mode” (async) | Test-mode success only – a live production transaction returns 000.000.000 |
| 000.200.000 | Transaction pending | Check status via GET /v1/payments/{id} |
Common Failure Codes
| Code | Meaning | Fix |
|---|---|---|
| 200.100.603 | Token decryption error | Wrong cert/key pair. Check merchant ID in cert matches Apple’s record. |
| 100.100.400 | Missing card.holder | Do not send card.holder for Apple Pay – remove the field entirely. |
| 100.100.600 | CVV required | Your entity needs CVV optional enabled. Contact PAYSTRAX support. |
| 600.200.500 | Brand not configured | APPLEPAY or GOOGLEPAY not in the entity’s supported brands. Contact PAYSTRAX. |
| 200.300.404 | Invalid or missing parameter (often an invalid or expired checkout session) | Invalid or missing parameter (often an invalid or expired checkout session) |
Webhooks
PAYSTRAX sends payment results to the webhook URL you configure in your dashboard. The webhook body is encrypted – do not parse it as plain JSON.
How to read a webhook:
- Read the initialization vector from the header
X-Initialization-Vector. - Read the authentication tag from the header
X-Authentication-Tag. - The request body is a hex-encoded string encrypted with AES-256-GCM. Decrypt it using your 64-character hex webhook secret (from the dashboard), the IV, and the auth tag.
- The result is the JSON payload shown below.
Delivery rules: respond with HTTP 2xx within 30 seconds. If you do not, PAYSTRAX retries, and you may receive duplicate or out-of-order notifications – de-duplicate on payload.id + result.code. Full reference: https://docs.paystrax.net/tutorials/webhooks
{
"type": "PAYMENT",
"payload": {
"id": "8ac9a4a49d905f0d...",
"paymentType": "DB",
"paymentBrand": "VISA", // actual card network (not APPLEPAY/GOOGLEPAY)
"amount": "10.00",
"currency": "EUR",
"result": {
"code": "000.000.000",
"description": "Transaction succeeded"
},
"card": {
"bin": "535558",
"last4Digits": "0548",
"expiryMonth": "02",
"expiryYear": "2029"
},
"threeDSecure": {
"eci": "07",
"verificationId": "..."
},
"customParameters": {
"APPLEPAY_Source": "Web",
"APPLEPAY_TokenVersion": "EC_v1"
},
"shortId": "8691.6050.5668",
"timestamp": "2026-04-15 12:55:16+0000"
}
}Testing
Apple Pay Testing
Device Requirements
Apple Pay on the web only works in Safari on:
- iPhone with Face ID or Touch ID
- iPad with Face ID or Touch ID
- MacBook with Touch ID
- Mac with Apple Watch paired
Certificate Expiry
Payment Processing Certificates issued by Apple expire after 25 months. Set a calendar reminder. When it expires, Apple Pay decryption will silently fail with garbage output. Renew by generating a new key pair and uploading a new CSR – no downtime if done before expiry.
Google Pay Testing
TEST Environment
Set environment: 'TEST' in your PaymentsClient configuration. In TEST mode:
- Works on any device with Chrome
- Works on localhost (HTTP or HTTPS)
- Returns dummy tokens – no real charges
- No Google merchant ID verification required
Going to PRODUCTION
Before switching to environment: 'PRODUCTION':
- Register at Google Pay Business Console
- Get a Google Merchant ID for your
merchantInfoconfig - Complete the Google integration checklist
- Ensure your PAYSTRAX entity has GOOGLEPAY brand enabled
Troubleshooting
Apple Pay button not appearing
- Using Safari (not Chrome, Firefox, Edge)
- Device has a card added to Apple Wallet
- Domain is registered with PAYSTRAX (or in your Apple Developer account)
- Verification file accessible at
/.well-known/apple-developer-merchantid-domain-association - Site served over HTTPS
ApplePaySession.canMakePayments()returns true in console
Merchant validation fails
- PAYSTRAX has a Merchant Identity Certificate in BIP (contact support if unsure)
- Your domain is registered in PAYSTRAX BIP
- The PAYSTRAX checkout session creation succeeds (check
checkoutIdin your server logs) - You are sending the exact
validationURLfrom Apple’sonvalidatemerchantevent - The
initiativeContextmatches the domain serving the page (no trailing slash, no protocol)
Decryption returns not valid JSON
The KDF produced the wrong symmetric key. Verify each parameter:
- Cipher is
aes-256-gcm(NOT aes-128-ctr) - partyU is the literal string
"Apple"(5 ASCII bytes) - partyV is the 32 merchant ID bytes from OID
1.2.840.113635.100.6.32in the cert - Auth tag is the last 16 bytes of the encrypted data, split before passing to openssl_decrypt
- The private key and certificate belong to the same key pair
- Certificate has not expired (Apple certs expire after 25 months)
100.100.600 – CVV required
Your PAYSTRAX entity is configured to require CVV for all card transactions. Apple Pay and Google Pay tokens do not contain CVV – you need to ask PAYSTRAX to enable CVV optional on your entity/channel for wallet transactions.
Contact support@paystrax.com and request CVV optional to be enabled for APPLEPAY and GOOGLEPAY payment brands.
Google Pay button not appearing
- Google Pay JS API script loaded (
pay.google.com/gp/p/js/pay.js) - Using Chrome (desktop or mobile) or a browser that supports Google Pay
- A payment method is saved in your Google account
- Check browser console for
isReadyToPayerrors - For PRODUCTION: Google Merchant ID is valid and approved
600.200.500 – Brand not configured
The payment brand (APPLEPAY or GOOGLEPAY) is not enabled on your PAYSTRAX entity. Contact support@paystrax.com and request that the wallet brand be added to your channel configuration.