Paystrax company logo
Server-to-Server Integration

Server-to-Server Integration | Apple Pay & Google Pay

Full native control over the payment flow. Your frontend triggers the wallet, your server decrypts tokens and submits card data directly to the PAYSTRAX API.

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.

1

User taps Apple Pay / Google Pay button

2

Your server calls PAYSTRAX to validate merchant (Apple Pay) or receives Google Pay token

3

Payment sheet appears, user authorizes with Face ID / Touch ID / biometrics

4

Your server decrypts the EC_v1 token (Apple Pay) or extracts Google Pay token

5

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 entityId and access token (from your PAYSTRAX dashboard).
  • Channel configuration: the APPLEPAY and/or GOOGLEPAY brands 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.

Recommended

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 started
Alternative

Option 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 started

Option A – No Apple Developer Account

1

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-association verification 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.

2

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

.htaccess (Apache)
<Files "apple-developer-merchantid-domain-association">
    ForceType application/octet-stream
</Files>
nginx.conf
location /.well-known/apple-developer-merchantid-domain-association {
    default_type application/octet-stream;
}
3

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:

Terminal
# 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
File layout
# 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 all

Option B – Your Own Apple Developer Account

1

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.

2

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:

Terminal
# 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.pem

Upload 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.

3

Register Your Domain

Apple Pay on the Web – Add Domain

Under your Merchant ID – Apple Pay on the WebAdd Domain. Apple provides a domain verification file. Host it at /.well-known/apple-developer-merchantid-domain-association and complete verification.

4

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

checkout.html
<!-- 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.

api/applepay_validate.php
<?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,
]);
config.php
<?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.

api/applepay_decrypt.php
<?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
api/checkout_payment.php
<?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)

POST /v1/payments
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                 = DB

Google 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.

1

Load Google Pay JS API

Include the Google Pay library on your checkout page

checkout.html
<script src="https://pay.google.com/gp/p/js/pay.js"></script>
<div id="google-pay-container"></div>
2

Configure PaymentsClient

Set environment and payment method parameters

checkout.html
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
});
3

Check Readiness and Show Button

Verify Google Pay is available before displaying

checkout.html
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);
});
4

Handle Payment

Load payment data and extract token

checkout.html
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);
    });
}
5

Submit to PAYSTRAX

POST the token to /v1/payments on your server

checkout.html
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));
}
6

Server-Side: POST to PAYSTRAX

PHP backend forwards the token directly

api/googlepay_payment.php
<?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)

POST /v1/payments
paymentBrand                  = GOOGLEPAY
virtualAccount.paymentToken   = {"signature":"MEUCIQDx...","protocolVersion":"ECv2",...}
entityId                      = 8ac9a4ca9d6d2134...
amount                        = 10.00
currency                      = EUR
paymentType                   = DB

Result Codes

Success Codes

CodeMeaningAction
000.000.000Transaction succeededPayment complete – show success page
000.100.110Request successfully processed in “Merchant in Integrator Test Mode” (async)Test-mode success only – a live production transaction returns 000.000.000
000.200.000Transaction pendingCheck status via GET /v1/payments/{id}

Common Failure Codes

CodeMeaningFix
200.100.603Token decryption errorWrong cert/key pair. Check merchant ID in cert matches Apple’s record.
100.100.400Missing card.holderDo not send card.holder for Apple Pay – remove the field entirely.
100.100.600CVV requiredYour entity needs CVV optional enabled. Contact PAYSTRAX support.
600.200.500Brand not configuredAPPLEPAY or GOOGLEPAY not in the entity’s supported brands. Contact PAYSTRAX.
200.300.404Invalid 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:

  1. Read the initialization vector from the header X-Initialization-Vector.
  2. Read the authentication tag from the header X-Authentication-Tag.
  3. 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.
  4. 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

Webhook payload (successful payment)
{
  "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

1

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
2

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

1

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
2

Going to PRODUCTION

Before switching to environment: 'PRODUCTION':

  • Register at Google Pay Business Console
  • Get a Google Merchant ID for your merchantInfo config
  • Complete the Google integration checklist
  • Ensure your PAYSTRAX entity has GOOGLEPAY brand enabled

Troubleshooting

1

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
2

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 checkoutId in your server logs)
  • You are sending the exact validationURL from Apple’s onvalidatemerchant event
  • The initiativeContext matches the domain serving the page (no trailing slash, no protocol)
3

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.32 in 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)
4

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.

5

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 isReadyToPay errors
  • For PRODUCTION: Google Merchant ID is valid and approved
6

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.