Developer Documentation

Musqet Payment Integration - Developer Documentation#

Overview#

This documentation covers how to integrate Musqet payments directly (without a CMS plugin) using standalone PHP examples. Three example directories are provided:

Gateway Examples Directory Payment Methods
Cardstream cardstream-gateway-examples/ Card (Hosted Fields), Google Pay, Apple Pay
Encoded encoded-gateway-examples/ Card (HPF), Google Pay, Apple Pay
Bitcoin bitcoin-examples/ Bitcoin (gateway-agnostic)

The two gateways differ significantly in their architecture:

Cardstream Encoded
Authentication SHA-512 signature over fields OAuth 2.0 (JWT tokens)
Request format Form-encoded JSON
Card JS library hostedfields.js (jQuery plugin) hpf-3.1.2.min.js (custom events on document)
Success check responseCode === '0' status in ['succeeded','paid','processed']
Country codes ISO 3166-1 numeric (826, 840) ISO 3166-1 alpha-3 (GBR, USA)
Currency codes ISO 4217 numeric (826, 840) ISO 4217 alpha (GBP, USD)
Google Pay Yes (via native Google Pay JS API) Yes (via APM library)
Apple Pay Yes (via native ApplePaySession API) Yes (via native ApplePaySession API or APM library)

Cardstream Gateway#

This section covers integration with the Cardstream gateway Cardstream uses SHA-512 signature authentication (no OAuth), form-encoded requests, and the hostedfields.js jQuery plugin for embedded card payments.

Two checkout modes are supported:

  1. Redirect Mode - Customer is redirected to Cardstream's hosted payment page
  2. Embedded Mode - Cardstream's hosted fields are embedded directly in your checkout page (requires jQuery)
  3. Google Pay - Native Google Pay button using Google's Pay API, processed via Cardstream's /direct/ endpoint
  4. Apple Pay - Native Apple Pay button using Safari's ApplePaySession API, processed via Cardstream's /direct/ endpoint

Table of Contents (Cardstream)#

  1. Gateway URL & IP Whitelisting
  2. Authentication (Signature)
  3. Redirect Mode
  4. Embedded Mode (Hosted Fields)
  5. Google Pay
  6. Apple Pay
  7. 3D Secure
  8. Refunds
  9. Webhooks / Callbacks
  10. Numeric Code Formats
  11. Integration Testing
  12. Running the Cardstream Examples

Cardstream Gateway URL#

The Cardstream gateway URL is specific to the Payment Services Provider. For Musqet, the gateway is always https://payments.musqet.tech:

Path Usage
https://payments.musqet.tech/direct/ Server-to-server transactions (requires IP whitelisting)
https://payments.musqet.tech/hosted/ Redirect / Hosted Payment Page (no IP whitelisting required)
https://payments.musqet.tech/sdk/web/v1/js/hostedfields.min.js Hosted Fields JavaScript library

IP Whitelisting#

Cardstream requires your server's IP address to be whitelisted before it will accept /direct/ requests. Without whitelisting, all server-to-server requests will be rejected.

This affects:

To get your IP whitelisted, contact your Musqet representative. If your infrastructure uses multiple outbound IPs (e.g., load balancers, NAT gateways, or CI/CD pipelines), all of them must be whitelisted.

The only integration that works without IP whitelisting is the Hosted Payment Page (/hosted/) redirect mode, where the customer's browser communicates directly with the gateway and no server-to-server calls are made.


Cardstream Authentication (Signature)#

Cardstream does not use OAuth or bearer tokens. Instead, every request is authenticated with a SHA-512 signature computed over the sorted, URL-encoded fields plus the shared secret.

Signature Algorithm#

function generateSignature($fields, $sharedSecret) {
    // 1. Remove any existing signature
    unset($fields['signature']);

    // 2. Sort alphabetically by key
    ksort($fields);

    // 3. Build URL-encoded query string
    $query = http_build_query($fields, '', '&');

    // 4. Append the shared secret (no separator)
    // 5. Hash with SHA-512
    return hash('sha512', $query . $sharedSecret);
}

Verifying a Response Signature#

function verifyResponseSignature($response, $sharedSecret) {
    $receivedSig = $response['signature'] ?? '';
    unset($response['signature']);
    $expectedSig = generateSignature($response, $sharedSecret);
    return hash_equals($expectedSig, $receivedSig);
}

Cardstream Redirect Mode#

The simplest integration. Build signed form fields and POST them to the gateway's hosted payment page.

Flow#

  1. Build transaction fields with your merchant ID, order details, and redirect URL
  2. Sign the fields with SHA-512
  3. Render an auto-submit HTML form POSTing to https://payments.musqet.tech/hosted/
  4. Customer completes payment on the gateway's hosted page
  5. Gateway redirects back to your redirectURL with a signed POST containing the result

Building Redirect Fields (Backend)#

$api = new CardstreamApi($merchantId, $sharedSecret, $gatewayUrl);

$fields = [
    'merchantID'        => $merchantId,
    'action'            => 'SALE',
    'type'              => '1',      // 1 = ecommerce, 2 = MOTO
    'amount'            => '9999',   // Minor units (pence/cents)
    'currencyCode'      => '826',    // ISO 4217 numeric (826 = GBP)
    'countryCode'       => '826',    // ISO 3166-1 numeric (826 = GB)
    'orderRef'          => 'ORDER_123',
    'transactionUnique' => uniqid('ORDER_123-'),
    'redirectURL'       => 'https://yoursite.com/api/webhook.php',
    'customerName'      => 'John Doe',
    'customerEmail'     => 'john@example.com',
    'customerAddress'   => '123 Test Street',
    'customerTown'      => 'London',
    'customerPostCode'  => 'SW1A 1AA',
    'customerCountryCode' => '826',
];

$fields['signature'] = $api->generateSignature($fields);

Auto-Submit Form (Frontend)#

<form id="redirect-form" method="POST" action="https://payments.musqet.tech/hosted/">
    <input type="hidden" name="merchantID" value="...">
    <input type="hidden" name="action" value="SALE">
    <!-- ... all signed fields ... -->
    <input type="hidden" name="signature" value="...">
</form>
<script>document.getElementById('redirect-form').submit();</script>

Handling the Redirect Callback#

// The gateway POSTs form-encoded data back to your redirectURL
$response = $_POST;

// Always verify the signature first
if (!$api->verifyResponseSignature($response)) {
    die('Invalid signature');
}

if ($response['responseCode'] === '0') {
    // Payment successful
    $xref = $response['xref']; // Save for refunds
} else {
    // Payment failed
    echo 'Error: ' . $response['responseMessage'];
}

Cardstream Embedded Mode (Hosted Fields)#

Hosted fields embed secure, PCI-compliant card input fields in your checkout page using the hostedfields.js jQuery plugin.

Flow#

  1. Frontend: Load jQuery + hostedfields.js from the gateway
  2. Frontend: Initialize the jQuery hostedForm plugin on your form
  3. Frontend: User enters card details into hosted field iframes
  4. Frontend: Call getPaymentDetails() to get a paymentToken
  5. Backend: Send paymentToken + order details to https://payments.musqet.tech/direct/
  6. Backend: Handle response (success, 3DS, or error)

Prerequisites#

HTML Structure#

<!-- jQuery is required by hostedfields.js -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

<form id="payment-form">
    <div class="form-group">
        <label>Card Number</label>
        <input type="hostedfield:cardNumber" placeholder="4444 3333 2222 1111">
    </div>
    <div class="form-group">
        <label>Expiry Date</label>
        <input type="hostedfield:cardExpiryDate" placeholder="MM/YY">
    </div>
    <div class="form-group">
        <label>CVV</label>
        <input type="hostedfield:cardCVV" placeholder="123">
    </div>
    <button type="submit" id="pay-button">Pay Now</button>
</form>

Note the special type attributes: hostedfield:cardNumber, hostedfield:cardExpiryDate, hostedfield:cardCVV. These tell the library which fields to replace with secure iframes.

JavaScript Initialization#

// Initialize the jQuery hostedForm plugin
$('#payment-form').hostedForm({
    autoSetup: true,
    autoSubmit: false,  // We handle submission ourselves
    fields: {
        any: {
            nativeEvents: true
        }
    }
});

var hostedFormInstance = $('#payment-form').hostedForm('instance');

// Listen for events
$('#payment-form').on('hostedfield:ready', function() {
    console.log('Hosted fields ready');
});

$('#payment-form').on('hostedform:valid', function() {
    document.getElementById('pay-button').disabled = false;
});

$('#payment-form').on('hostedform:invalid', function() {
    // Form has validation errors
});

$('#payment-form').on('hostedform:error', function(e, data) {
    console.error('Error:', data);
});

Getting the Payment Token#

// On form submit, get the payment token
hostedFormInstance.getPaymentDetails({}, true).then(function(details) {
    if (details.success) {
        // Send details.paymentToken to your backend
        submitToBackend(details.paymentToken);
    } else {
        alert(details.message || 'Card validation failed.');
    }
}).catch(function(err) {
    alert(err.message || 'Payment failed.');
});

Processing the Token (Backend)#

$api = new CardstreamApi($merchantId, $sharedSecret, $gatewayUrl);

$fields = [
    'merchantID'         => $merchantId,
    'action'             => 'SALE',
    'type'               => '1',
    'amount'             => (string) $api->convertAmountToMinor($amount),
    'currencyCode'       => (string) $api->getCurrencyCode($currency),
    'countryCode'        => (string) $api->getCountryCodeNumeric($billingCountry),
    'orderRef'           => $orderId,
    'transactionUnique'  => uniqid($orderId . '-'),
    'redirectURL'        => $callbackUrl,
    'paymentToken'       => $paymentToken,      // From hostedfields.js
    'deviceIpAddress'    => $_SERVER['REMOTE_ADDR'],
    'threeDSRedirectURL' => $threeDSCallbackUrl, // For 3DS redirects
    'customerName'       => 'John Doe',
    'customerEmail'      => 'john@example.com',
    // ... other customer fields
];

$response = $api->sendDirectTransaction($fields);

if ($api->isSuccess($response)) {
    // responseCode === '0' — payment successful
    $xref = $response['xref']; // Save for refunds
} elseif ($api->requires3ds($response)) {
    // responseCode === '65802' — 3DS required
    // See 3D Secure section below
} else {
    // Payment failed
    $error = $response['responseMessage'];
}

Cardstream Google Pay#

This documentation describes Google Pay support for embedded mode. In redirect mode, Google Pay is supported by the gateway directly via the hosted payment page. The embedded integration uses Google's native Pay API (pay.google.com/gp/p/js/pay.js) on the frontend, with the token sent to Cardstream's /direct/ endpoint using the paymentMethod=googlepay field.

Flow#

  1. Frontend loads Google's Pay API script
  2. Create a PaymentsClient and call isReadyToPay() to check device/browser support
  3. Render the Google Pay button
  4. Customer clicks button → Google Pay sheet opens → customer authorizes
  5. Frontend receives PaymentData with token at paymentMethodData.tokenizationData.token
  6. Frontend sends the raw token string to your backend
  7. Backend POSTs to Cardstream /direct/ with paymentMethod=googlepay + paymentToken=<token> alongside standard transaction fields
  8. Cardstream returns standard response (responseCode=0 for success, 65802 for 3DS)

Frontend: Initialize Google Pay#

<script src="https://pay.google.com/gp/p/js/pay.js"></script>

<script>
// Tokenization specification — gateway must be 'crst'
function getTokenizationSpecification() {
    return {
        type: 'PAYMENT_GATEWAY',
        parameters: {
            gateway: 'crst',
            gatewayMerchantId: 'YOUR_CARDSTREAM_MERCHANT_ID'
        }
    };
}

// Allowed payment methods
function getBaseCardPaymentMethod() {
    return {
        type: 'CARD',
        parameters: {
            allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
            allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX']
        }
    };
}

// Create client — use 'TEST' for sandbox, 'PRODUCTION' for live
var paymentsClient = new google.payments.api.PaymentsClient({
    environment: 'TEST'
});

// Check if Google Pay is available
paymentsClient.isReadyToPay({
    apiVersion: 2,
    apiVersionMinor: 0,
    allowedPaymentMethods: [getBaseCardPaymentMethod()]
}).then(function(response) {
    if (response.result) {
        // Mount the Google Pay button
        var button = paymentsClient.createButton({
            onClick: onGooglePayClicked,
            buttonColor: 'black',
            buttonType: 'pay',
            buttonSizeMode: 'fill'
        });
        document.getElementById('googlepay-container').appendChild(button);
    }
});
</script>

Frontend: Handle Google Pay Button Click#

function onGooglePayClicked() {
    var paymentDataRequest = {
        apiVersion: 2,
        apiVersionMinor: 0,
        allowedPaymentMethods: [{
            ...getBaseCardPaymentMethod(),
            tokenizationSpecification: getTokenizationSpecification()
        }],
        transactionInfo: {
            totalPriceStatus: 'FINAL',
            totalPrice: '99.99',
            currencyCode: 'GBP',
            countryCode: 'GB'
        },
        merchantInfo: {
            merchantId: 'YOUR_GOOGLE_PAY_MERCHANT_ID', // 'TEST' for sandbox
            merchantName: 'Your Store Name'
        }
    };

    paymentsClient.loadPaymentData(paymentDataRequest)
        .then(function(paymentData) {
            // Extract the token and send to backend
            var token = paymentData.paymentMethodData.tokenizationData.token;
            submitGooglePayToken(token);
        })
        .catch(function(err) {
            if (err.statusCode === 'CANCELED') return; // User cancelled
            console.error('Google Pay error:', err);
        });
}

function submitGooglePayToken(token) {
    fetch('/api/process-googlepay.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            google_pay_token: token,
            order_id: 'ORDER_123',
            amount: 99.99,
            currency: 'GBP',
            billing: {
                firstName: 'John',
                lastName: 'Doe',
                email: 'john@example.com',
                address1: '123 Test Street',
                city: 'London',
                countryCode: 'GB',
                postcode: 'SW1A 1AA'
            }
        })
    })
    .then(r => r.json())
    .then(function(result) {
        if (result.success) {
            window.location.href = result.redirect_url;
        } else if (result.requires_3ds) {
            handle3DS(result); // Same 3DS flow as card payments
        } else {
            alert(result.error || 'Payment failed');
        }
    });
}

Backend: Process Google Pay Payment#

// process-googlepay.php
$input = json_decode(file_get_contents('php://input'), true);
$googlePayToken = $input['google_pay_token'];

$api = new CardstreamApi($merchantId, $sharedSecret, $gatewayUrl);

$fields = [
    'merchantID'         => $merchantId,
    'action'             => 'SALE',
    'type'               => '1',
    'amount'             => (string) $api->convertAmountToMinor($amount),
    'currencyCode'       => (string) $api->getCurrencyCode($currency),
    'countryCode'        => (string) $api->getCountryCodeNumeric($billingCountry),
    'orderRef'           => $orderId,
    'transactionUnique'  => uniqid($orderId . '-'),
    'redirectURL'        => $callbackUrl,
    'paymentMethod'      => 'googlepay',              // Required: identifies Google Pay
    'paymentToken'       => $googlePayToken,           // Required: raw token from Google Pay
    'deviceIpAddress'    => $_SERVER['REMOTE_ADDR'],
    'threeDSRedirectURL' => $threeDSCallbackUrl,
    'customerName'       => 'John Doe',
    'customerEmail'      => 'john@example.com',
    // ... other customer fields
];

$response = $api->sendDirectTransaction($fields);

if ($api->isSuccess($response)) {
    // responseCode === '0' — payment successful
    $xref = $response['xref'];
} elseif ($api->requires3ds($response)) {
    // responseCode === '65802' — 3DS required (same flow as card)
} else {
    $error = $response['responseMessage'];
}

Key Differences from Card Payments#

Card (Hosted Fields) Google Pay
Frontend library hostedfields.js (jQuery) Google Pay API (pay.js)
Token source getPaymentDetails() callback loadPaymentData() response
Backend field paymentToken (from hosted fields) paymentMethod=googlepay + paymentToken (from Google)
3DS handling Same Same
Redirect mode Supported Not needed (Google Pay only in embedded)

Configuration#

Setting Description
googlepay_merchant_id Google Pay merchant ID from Google Business Console. Use TEST in sandbox.
googlepay_gateway_merchant_id Gateway merchant ID for tokenization. Defaults to your Cardstream merchant ID.

Cardstream Apple Pay#

This documentation describes Apple Pay support for embedded mode. In redirect mode, Apple Pay is supported by the gateway directly via the hosted payment page. The embedded integration uses Safari's native ApplePaySession API on the frontend, with server-side merchant validation via Apple's servers, and the payment token sent to Cardstream's /direct/ endpoint using the paymentMethod=applepay field.

Flow#

  1. Frontend checks ApplePaySession.canMakePayments() — hides option if unavailable
  2. User clicks Apple Pay button → new ApplePaySession(3, paymentRequest) with amount/currency
  3. onvalidatemerchant: frontend sends validationURL to backend → backend POSTs to Apple with merchant cert → returns merchant session → completeMerchantValidation()
  4. onpaymentauthorized: extract event.payment.token → stringify → store as payment token
  5. Backend receives token → sends to Cardstream /direct/ with paymentMethod=applepay, paymentToken=<token>
  6. Response: responseCode=0 success, 65802 3DS required, else error

Prerequisites#

See the Apple Pay Setup Guide above for detailed step-by-step instructions on all of the following:

Frontend: Apple Pay Button and Session#

// Check if Apple Pay is available
if (window.ApplePaySession && ApplePaySession.canMakePayments()) {
    document.getElementById('applepay-container').style.display = 'block';
}

function onApplePayClicked() {
    var session = new ApplePaySession(3, {
        countryCode: 'GB',
        currencyCode: 'GBP',
        total: { label: 'Store', amount: '10.00' },
        supportedNetworks: ['visa', 'masterCard', 'amex'],
        merchantCapabilities: ['supports3DS']
    });

    session.onvalidatemerchant = function(event) {
        // Send validation URL to backend for merchant validation
        fetch('/api/applepay-validate-merchant.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ validationURL: event.validationURL })
        })
        .then(function(r) { return r.json(); })
        .then(function(merchantSession) {
            session.completeMerchantValidation(merchantSession);
        })
        .catch(function(err) {
            console.error('Merchant validation failed:', err);
            session.abort();
        });
    };

    session.onpaymentauthorized = function(event) {
        var token = JSON.stringify(event.payment.token);
        // Send token to backend
        fetch('/api/process-applepay.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                apple_pay_token: token,
                order_id: 'ORDER_123',
                amount: 10.00,
                currency: 'GBP',
                billing: {
                    firstName: 'John',
                    lastName: 'Doe',
                    email: 'john@example.com',
                    address1: '123 Test Street',
                    city: 'London',
                    countryCode: 'GB',
                    postcode: 'SW1A 1AA'
                }
            })
        })
        .then(function(r) { return r.json(); })
        .then(function(result) {
            if (result.success) {
                session.completePayment(ApplePaySession.STATUS_SUCCESS);
                window.location.href = result.redirect_url;
            } else {
                session.completePayment(ApplePaySession.STATUS_FAILURE);
                alert(result.error || 'Payment failed');
            }
        });
    };

    session.begin();
}

Backend: Merchant Validation#

// applepay-validate-merchant.php
$input = json_decode(file_get_contents('php://input'), true);
$validationUrl = $input['validationURL'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $validationUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'merchantIdentifier' => $merchantId,
    'displayName' => 'Store Name',
    'initiative' => 'web',
    'initiativeContext' => $domain,
]));
curl_setopt($ch, CURLOPT_SSLCERT, $certPath);
curl_setopt($ch, CURLOPT_SSLKEY, $keyPath);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$merchantSession = json_decode(curl_exec($ch), true);

header('Content-Type: application/json');
echo json_encode($merchantSession);

Backend: Process Apple Pay Payment#

// process-applepay.php
$input = json_decode(file_get_contents('php://input'), true);
$applePayToken = $input['apple_pay_token'];

$api = new CardstreamApi($merchantId, $sharedSecret, $gatewayUrl);

$fields = [
    'merchantID'         => $merchantId,
    'action'             => 'SALE',
    'type'               => '1',
    'amount'             => (string) $api->convertAmountToMinor($amount),
    'currencyCode'       => (string) $api->getCurrencyCode($currency),
    'countryCode'        => (string) $api->getCountryCodeNumeric($billingCountry),
    'orderRef'           => $orderId,
    'transactionUnique'  => uniqid($orderId . '-'),
    'redirectURL'        => $callbackUrl,
    'paymentMethod'      => 'applepay',               // Required: identifies Apple Pay
    'paymentToken'       => $applePayToken,            // Required: stringified token from Apple Pay
    'deviceIpAddress'    => $_SERVER['REMOTE_ADDR'],
    'threeDSRedirectURL' => $threeDSCallbackUrl,
    'customerName'       => 'John Doe',
    'customerEmail'      => 'john@example.com',
    // ... other customer fields
];

$response = $api->sendDirectTransaction($fields);

if ($api->isSuccess($response)) {
    // responseCode === '0' — payment successful
    $xref = $response['xref'];
} elseif ($api->requires3ds($response)) {
    // responseCode === '65802' — 3DS required (same flow as card)
} else {
    $error = $response['responseMessage'];
}

Key Differences from Google Pay#

Google Pay Apple Pay
Frontend API Google Pay JS API (pay.js) ApplePaySession (native Safari)
Availability check isReadyToPay() ApplePaySession.canMakePayments()
Merchant validation Not required Required (server-side with merchant cert)
Token source paymentMethodData.tokenizationData.token event.payment.token (stringified)
Backend field paymentMethod=googlepay paymentMethod=applepay
Browser support Chrome, Safari, Firefox, Edge Safari (macOS/iOS), Chrome on iOS 16+
Certificate required No Yes (Apple Merchant Identity Certificate)

Configuration#

Setting Description
applepay_merchant_id Apple Pay merchant identifier from Apple Developer account
applepay_merchant_cert Path or PEM content of the Merchant Identity Certificate
applepay_merchant_key Path or PEM content of the Merchant Identity Private Key

Cardstream 3D Secure#

When a /direct/ transaction returns responseCode=65802, 3D Secure authentication is required. The 3DS2 flow involves multiple steps — device fingerprinting, then (if needed) a visible challenge — each requiring a continuation request back to the gateway.

Initial Transaction Setup#

Your initial SALE request must include:

Flow#

  1. Initial SALE returns responseCode=65802 with threeDSURL, threeDSRef, and threeDSRequest
  2. Device fingerprinting: If threeDSRequest contains threeDSMethodData, this is a silent fingerprinting step. Submit it in a hidden iframe; the ACS will POST back to your threeDSRedirectURL (with ?threeDSAcsResponse=method appended)
  3. Fingerprinting continuation: Your callback sends threeDSRef + threeDSResponse to /direct/. The gateway may return success, failure, or another 65802 requiring a visible challenge
  4. Visible challenge: If the response contains threeDSRequest with creq, display the ACS challenge page in an overlay/iframe targeting the threeDSURL
  5. Challenge continuation: After the user authenticates, the ACS POSTs cres to your threeDSRedirectURL. Your callback sends a final continuation to /direct/
  6. Result: The gateway returns responseCode=0 (success) or an error code (failure)

Storing the threeDSRef#

The threeDSRef changes at each step. You must store it between requests. PHP sessions won't work for the callback because the ACS POST comes from a cross-origin iframe (SameSite cookie restrictions block session cookies). Use server-side storage keyed by order ID instead:

// Store (after each gateway response that returns a new threeDSRef)
$refFile = sys_get_temp_dir() . '/3ds_' . md5($orderId) . '.ref';
file_put_contents($refFile, $response['threeDSRef']);

// Retrieve (in the callback)
$storedRef = file_exists($refFile) ? trim(file_get_contents($refFile)) : '';

Handling 3DS Response (Frontend)#

When the backend returns a 3DS response, detect whether it is a fingerprinting step or a visible challenge:

function handle3DS(result) {
    var isFingerprint = result.threeds_request
        && result.threeds_request.threeDSMethodData;

    if (isFingerprint) {
        // Hidden iframe — device fingerprinting (invisible to customer)
        var iframe = document.createElement('iframe');
        iframe.name = 'threeds_fingerprint';
        iframe.style.cssText = 'width:1px;height:1px;visibility:hidden;';
        document.body.appendChild(iframe);

        var form = document.createElement('form');
        form.method = 'POST';
        form.target = 'threeds_fingerprint';
        form.action = result.threeds_url;

        for (var key in result.threeds_request) {
            var input = document.createElement('input');
            input.type = 'hidden';
            input.name = key;
            input.value = result.threeds_request[key];
            form.appendChild(input);
        }
        document.body.appendChild(form);
        form.submit();

        // Timeout fallback — if ACS doesn't respond within 10 seconds,
        // submit a timeout indicator to the callback
        setTimeout(function() {
            var f = document.createElement('form');
            f.method = 'POST';
            f.action = result.callback_url + '&threeDSAcsResponse=method';
            var i = document.createElement('input');
            i.type = 'hidden'; i.name = 'threeDSMethodData'; i.value = 'timeout';
            f.appendChild(i);
            document.body.appendChild(f);
            f.submit();
        }, 10000);
    } else {
        // Visible challenge — show in an overlay
        var overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;'
            + 'height:100%;background:rgba(0,0,0,0.5);z-index:99999;'
            + 'display:flex;align-items:center;justify-content:center;';
        var box = document.createElement('div');
        box.style.cssText = 'background:white;border-radius:8px;padding:20px;'
            + 'width:90%;max-width:500px;max-height:90vh;overflow:auto;';
        var iframe = document.createElement('iframe');
        iframe.name = 'threeds_challenge';
        iframe.style.cssText = 'width:100%;min-height:420px;border:none;';
        box.appendChild(iframe);
        overlay.appendChild(box);
        document.body.appendChild(overlay);

        var form = document.createElement('form');
        form.method = 'POST';
        form.target = 'threeds_challenge';
        form.action = result.threeds_url;
        form.style.display = 'none';

        for (var key in result.threeds_request) {
            var input = document.createElement('input');
            input.type = 'hidden';
            input.name = key;
            input.value = result.threeds_request[key];
            form.appendChild(input);
        }
        document.body.appendChild(form);
        form.submit();
    }
}

3DS Callback Handler (Backend)#

The callback (threeds-callback.php) handles both fingerprinting and challenge responses. It detects the step via the threeDSAcsResponse=method query parameter:

$orderId = $_GET['order_id'] ?? 'unknown';
$isMethodResponse = ($_GET['threeDSAcsResponse'] ?? '') === 'method';

// Retrieve stored threeDSRef
$refFile = sys_get_temp_dir() . '/3ds_' . md5($orderId) . '.ref';
$storedRef = file_exists($refFile) ? trim(file_get_contents($refFile)) : '';

if ($isMethodResponse) {
    // Device fingerprinting completed
    $threeDSRef = $storedRef;
    $threeDSResponse = !empty($_POST) ? $_POST : ['threeDSMethodData' => 'timeout'];
} else {
    // Challenge completed — forward all ACS response fields
    $threeDSRef = $_POST['threeDSRef'] ?? $_POST['MD'] ?? $storedRef;
    $threeDSResponse = $_POST;
    unset($threeDSResponse['threeDSRef'], $threeDSResponse['MD']);
}

// Clean up ref file
if (file_exists($refFile)) { @unlink($refFile); }

// Build continuation request
$fields = [
    'merchantID' => $config['merchant_id'],
    'action'     => 'SALE',
    'threeDSRef' => $threeDSRef,
];
foreach ($threeDSResponse as $key => $value) {
    $fields['threeDSResponse[' . $key . ']'] = $value;
}

$response = $api->sendDirectTransaction($fields);

if ($api->isSuccess($response)) {
    // Payment successful — redirect to success page
} elseif ($api->requires3ds($response)) {
    // Another 3DS step needed (e.g., fingerprinting returned a challenge)
    // Store the new threeDSRef and render the appropriate iframe/overlay
    file_put_contents($refFile, $response['threeDSRef']);
    // ... render fingerprinting or challenge iframe
} else {
    // Payment failed — redirect to error page
}

3DS Device Fields#

The initial SALE request must include device fields for 3DS2 risk assessment. Fields derived from HTTP headers can be read server-side; fields only available in the browser (timezone, screen resolution, OS) should be collected via JavaScript and sent to your backend:

Field Source Example
deviceChannel Always browser browser
deviceType Always desktop desktop
deviceIdentity $_SERVER['HTTP_USER_AGENT'] Mozilla/5.0...
deviceAcceptContent $_SERVER['HTTP_ACCEPT'] */*
deviceAcceptEncoding $_SERVER['HTTP_ACCEPT_ENCODING'] gzip, deflate, br
deviceAcceptLanguage $_SERVER['HTTP_ACCEPT_LANGUAGE'] en-GB,en;q=0.9
deviceAcceptCharset $_SERVER['HTTP_ACCEPT_CHARSET'] or UTF-8 UTF-8
deviceIpAddress $_SERVER['REMOTE_ADDR'] 81.111.22.186
deviceCapabilities Always javascript javascript
deviceTimeZone JS: new Date().getTimezoneOffset() 0
deviceScreenResolution JS: screen.width+'x'+screen.height+'x'+screen.colorDepth 1920x1080x24
deviceOperatingSystem JS: parse from navigator.userAgent macos

3DS Authentication Statuses#

After authentication, the gateway's threeDSAuthenticated field indicates the outcome. Your merchant account's threeDSCheckPref setting determines which statuses are accepted:

Status Meaning Typical outcome
Y Fully authenticated Accepted
A Attempted authentication Accepted (if configured)
N Not authenticated Declined
R Rejected by issuer Declined (must not retry)
U Unable to authenticate Merchant's discretion
E Error during checks Merchant's discretion
I Information only No authentication

You do not need to check threeDSAuthenticated directly — the gateway evaluates it against your preferences and returns responseCode=0 (accepted) or an error code (declined).


Cardstream Refunds#

Refunds are processed via the /direct/ endpoint using the REFUND_SALE action. This references the original transaction by its xref (cross-reference), which is returned in every successful payment response.

Important: Save the xref from every successful payment — it is the only way to issue a refund later.

Refund Request#

$fields = [
    'merchantID' => $merchantId,
    'action'     => 'REFUND_SALE',
    'type'       => '1',
    'xref'       => $originalXref,        // xref from the original SALE
    'amount'     => $amountInPence,        // Amount in minor units (pence/cents)
];

$fields['signature'] = generateSignature($fields, $sharedSecret);
$response = $api->sendDirectTransaction($fields);

Partial Refunds#

To issue a partial refund, set amount to less than the original transaction amount. Multiple partial refunds can be issued against the same xref up to the original transaction total.

Refund Response#

The response uses the same format as payment responses:

A successful refund returns its own xref which can be used for tracking.

Example (API Endpoint)#

See api/process-refund.php in the examples:

// POST JSON: { "xref": "26032810ZS38YG39DZ30YZB", "amount": 10.00 }
$response = $api->sendRefund($xref, $amount);

if ($api->isSuccess($response)) {
    echo json_encode(['success' => true, 'refund_xref' => $response['xref']]);
} else {
    echo json_encode(['success' => false, 'error' => $api->extractErrorMessage($response)]);
}

Example (Frontend)#

The examples include a standalone refund page at frontend/refund.html with a simple form for xref and amount. A link to the refund page is shown on the success page after each payment.


Cardstream Webhooks / Callbacks#

Cardstream sends form-encoded POST data (not JSON) to your redirectURL. Always verify the SHA-512 signature before processing.

// webhook.php
$response = $_POST;  // Form-encoded, not JSON

$api = new CardstreamApi($merchantId, $sharedSecret, $gatewayUrl);

if (!$api->verifyResponseSignature($response)) {
    http_response_code(400);
    die('Invalid signature');
}

$orderRef = $response['orderRef'] ?? null;
$responseCode = $response['responseCode'] ?? null;
$xref = $response['xref'] ?? null;

if ($responseCode === '0') {
    // Payment successful
    // updateOrderStatus($orderRef, 'paid', $xref);
} else {
    // Payment failed
    // updateOrderStatus($orderRef, 'failed');
}

http_response_code(200);
echo 'OK';

Webhook Summary#

Field Details
Format Form-encoded POST ($_POST)
Verification SHA-512 signature verification required
Success check responseCode === '0'
Transaction ref xref (cross-reference string)

Cardstream Numeric Code Formats#

Cardstream uses numeric ISO codes throughout. Amounts are in minor units (pence/cents).

Currency Codes (ISO 4217 Numeric)#

Currency Alpha Numeric
British Pound GBP 826
US Dollar USD 840
Euro EUR 978
Australian Dollar AUD 36
Canadian Dollar CAD 124

Country Codes (ISO 3166-1 Numeric)#

Country Alpha-2 Numeric
United Kingdom GB 826
United States US 840
France FR 250
Germany DE 276
Italy IT 380

Amount Format#

Amounts are in minor currency units (pence/cents). Multiply the decimal amount by 100:

$amountMinor = (int) round((float) $amount * 100);
// 99.99 -> 9999

Cardstream Response Codes#

Code Meaning
0 Success
65802 3D Secure authentication required
Other Failure — see responseMessage for details

Cardstream Transaction Types#

Field Value Meaning
action SALE Authorise + capture
action REFUND_SALE Refund against an original transaction (requires xref)
type 1 Ecommerce
type 2 MOTO (Mail Order / Telephone Order)

Cardstream Integration Testing#

For test cards and specific amounts that trigger different response codes, see the Cardstream integration testing guide:

Integration Testing Reference

This guide covers test card numbers, test amounts for simulating approvals, declines, and specific error responses, and other testing scenarios.


Running the Cardstream Examples#

Important: Embedded mode, Google Pay, Apple Pay, and refunds all use the /direct/ endpoint, which requires your server's IP to be whitelisted by Cardstream. See IP Whitelisting. If you run the examples on your local machine, your local IP must be whitelisted — in most cases, you will need to run them on a remote server whose IP has already been whitelisted. The only mode that works without whitelisting is Redirect Mode (Hosted Payment Page).

Security Note: The example API endpoints use Access-Control-Allow-Origin: * for development convenience. In production, restrict this to your specific domain(s) or remove it entirely if your frontend and API are on the same origin.

cd Documentation/cardstream-gateway-examples
php -S 0.0.0.0:8080

Open http://<your-server>:8080 — the index.html redirects to the card checkout page. Use the Configure page to enter your credentials (or copy config.php to config.local.php and edit manually).

URL Description
http://localhost:8080/frontend/checkout-card.html Card payment (hosted fields)
http://localhost:8080/frontend/checkout-hosted.html Card payment (redirect / hosted payment page)
http://localhost:8080/frontend/checkout-googlepay.html Google Pay payment
http://localhost:8080/frontend/checkout-applepay.html Apple Pay payment (Safari/iOS only)
http://localhost:8080/frontend/refund.html Refund a previous transaction
http://localhost:8080/frontend/configure.html Configuration page
http://localhost:8080/redirect-mode.php Redirect mode (standalone auto-submit form)

Cardstream Example Files#

cardstream-gateway-examples/
├── index.html                     - Redirects to /frontend/checkout-card.html
├── config.php                     - Config template
├── config.local.php               - Your credentials (gitignored)
├── CardstreamApi.php              - Core API class (signatures, transactions, converters)
├── redirect-mode.php              - Redirect/HPP standalone demo
├── api/
│   ├── config.php                 - Config read/write API (for configure page)
│   ├── session.php                - Returns gateway config for hostedfields.js
│   ├── build-redirect.php         - Builds signed form fields for hosted payment page
│   ├── process-payment.php        - Receives paymentToken, POSTs to /direct/
│   ├── process-googlepay.php      - Google Pay token processing via /direct/
│   ├── process-applepay.php       - Apple Pay token processing via /direct/
│   ├── process-refund.php         - Refund processing via /direct/ (REFUND_SALE)
│   ├── applepay-validate-merchant.php - Apple Pay merchant validation proxy
│   ├── threeds-callback.php       - 3DS continuation handler (fingerprint + challenge)
│   └── webhook.php                - Callback handler with signature verification
└── frontend/
    ├── checkout-card.html         - Card checkout (hostedfields.js + jQuery)
    ├── checkout-hosted.html       - Card checkout (redirect / hosted payment page)
    ├── checkout-googlepay.html    - Google Pay checkout (native Google Pay API)
    ├── checkout-applepay.html     - Apple Pay checkout (ApplePaySession API)
    ├── configure.html             - Web UI for editing config.local.php
    ├── success.html               - Payment success page (includes refund link)
    ├── cancel.html                - Payment cancellation page
    └── refund.html                - Refund page (xref + amount form)


Encoded Gateway#

This section covers integration with the Encoded payment gateway. Card payments are processed through Encoded's Hosted Payment Fields (HPF) and Hosted Payment Page (HPP). Google Pay uses Encoded's Alternative Payment Methods (APM) library.

Two checkout modes are supported:

  1. Redirect Mode - Customer is redirected to Encoded's hosted payment page
  2. Embedded Mode - Encoded's payment fields are embedded directly in your checkout page

Table of Contents (Encoded)#

  1. Environments & URLs
  2. Authentication
  3. Redirect Mode (Hosted Payment Page)
  4. Embedded Mode
  5. Transaction API
  6. Webhooks / Callbacks
  7. Test Cards
  8. Running the Encoded Examples

Environments & URLs#

Encoded Gateway URLs#

Environment URL
Production https://pay.musqet.tech
Sandbox https://pay-test.musqet.tech

Encoded API URLs (For HPF & Redirect)#

Environment API URL Assets URL
Production https://pay.musqet.tech/api/v1/ https://pay.musqet.tech/assets/
Sandbox (SIT) https://pay-test.musqet.tech/api/v1/ https://pay-test.musqet.tech/assets/

APM (Google Pay / Apple Pay) uses the same gateway URLs as card payments.

Encoded Authentication URL#

Environment Auth URL
Production https://pay.musqet.tech/auth/oauth/token
Sandbox https://pay-test.musqet.tech/auth/oauth/token

Authentication#

The Encoded gateway uses OAuth 2.0 Client Credentials Grant for authentication. You will need:

Obtaining an Access Token#

function createAccessToken($merchantId, $secretKey, $isSandbox = true) {
    $authUrl = $isSandbox
        ? 'https://pay-test.musqet.tech/auth/oauth/token'
        : 'https://pay.musqet.tech/auth/oauth/token';

    $ch = curl_init($authUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
    curl_setopt($ch, CURLOPT_USERPWD, "$merchantId:$secretKey");
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/x-www-form-urlencoded'
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);
    return $data['access_token'];
}

Token Scopes#

Different operations require different token scopes:

Scope Usage
(default) Full API access for server-side operations
session_limited_read Limited token for Hosted Payment Fields (safe for frontend)
apm Token for Alternative Payment Methods (Google Pay, Apple Pay)

Redirect Mode (Encoded Hosted Payment Page)#

The simplest integration method. Redirect customers to Encoded's hosted payment page.

Flow#

  1. Create an order via API
  2. Redirect customer to the payment URL
  3. Customer completes payment
  4. Customer is redirected back to your success/cancel URL
  5. You receive a callback with payment status

Creating an Order (Backend)#

function createOrder($orderData, $merchantId, $secretKey, $isSandbox = true) {
    $accessToken = createAccessToken($merchantId, $secretKey, $isSandbox);

    $gatewayUrl = $isSandbox
        ? 'https://pay-test.musqet.tech/api/v1/orders'
        : 'https://pay.musqet.tech/api/v1/orders';

    $items = [];
    foreach ($orderData['items'] as $i => $item) {
        $items[$i] = [
            'object' => 'order.item',
            'ref' => $item['sku'],
            'description' => $item['name'],
            'quantity' => (int)$item['quantity'],
            'unitAmount' => (float)$item['price'],
            'taxUnitAmount' => 0.00,
            'taxRate' => 0.00,
            'totalAmount' => (float)($item['price'] * $item['quantity']),
            'taxAmount' => 0.00
        ];
    }

    $payload = [
        'object' => 'order',
        'ref' => $orderData['orderId'],
        'description' => 'Order #' . $orderData['orderId'],
        'currency' => $orderData['currency'] ?? 'GBP',
        'totalAmount' => (float)$orderData['amount'],
        'items' => $items,
        'billingCustomer' => [
            'object' => 'customer',
            'forename' => $orderData['billing']['firstName'],
            'surname' => $orderData['billing']['lastName'],
            'contact' => [
                'object' => 'contact',
                'email' => $orderData['billing']['email'],
                'phone' => [
                    'object' => 'phone',
                    'mobile' => $orderData['billing']['phone'] ?? ''
                ],
                'address' => [
                    'object' => 'address',
                    'forename' => $orderData['billing']['firstName'],
                    'surname' => $orderData['billing']['lastName'],
                    'line1' => $orderData['billing']['address1'],
                    'line2' => $orderData['billing']['address2'] ?? '',
                    'line3' => $orderData['billing']['city'],
                    'country' => $orderData['billing']['countryCode'], // ISO3: "GBR", "FRA", etc.
                    'postcode' => $orderData['billing']['postcode']
                ]
            ]
        ],
        'hpp' => [
            'view' => [
                'order' => ['type' => 'expanded']
            ],
            'returnUrl' => $orderData['successUrl']
        ]
    ];

    $ch = curl_init($gatewayUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    // Important: The API expects an array of orders
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([$payload]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json',
        'Authorization: Bearer ' . $accessToken,
        'Content-Type: application/json'
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);

    // Response is an array, get first order's HPP URL
    return $data[0]['links']['hpp']['v1'];
}

Redirect Customer#

// Frontend
function redirectToPayment(checkoutUrl) {
    window.location.href = checkoutUrl;
}

Handle Callback#

// callback.php - Endpoint receiving POST from Musqet gateway
$requestBody = file_get_contents('php://input');
$data = json_decode($requestBody, true);

$orderId = $data['orderId'] ?? null;
$status = $data['status'] ?? null;
$transactionId = $data['transactionId'] ?? null;

if ($status === 'succeeded' || $status === 'paid') {
    // Update order as paid
    updateOrderStatus($orderId, 'paid', $transactionId);
} else {
    // Handle failed payment
    updateOrderStatus($orderId, 'failed');
}

http_response_code(200);
echo 'OK';

Embedded Mode#

Encoded Hosted Payment Fields (Card Payments)#

Encoded's Hosted Payment Fields (HPF) provide secure, PCI-compliant card input fields embedded as iframes in your checkout page.

Integration Flow#

  1. Backend: Create a payment session
  2. Backend: Generate a session-limited JWT token
  3. Frontend: Initialize HPF JavaScript library
  4. Frontend: User enters card details
  5. Frontend: Sync session (save card data to session)
  6. Backend: Submit transaction using session ID

Step 1: Create Payment Session (Backend)#

function createSession($accessToken, $isSandbox = true) {
    $apiUrl = $isSandbox
        ? 'https://pay-test.musqet.tech/api/v1/sessions'
        : 'https://pay.musqet.tech/api/v1/sessions';

    $payload = [
        'object' => 'session',
        'fields' => ['pan', 'expiryDate', 'securityCode']
    ];

    $ch = curl_init($apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $accessToken,
        'Content-Type: application/json'
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true); // Returns session with 'id' field
}

Step 2: Generate Session-Limited JWT (Backend)#

function createHpfToken($merchantId, $secretKey, $sessionId, $isSandbox = true) {
    $authUrl = $isSandbox
        ? 'https://pay-test.musqet.tech/auth/oauth/token'
        : 'https://pay.musqet.tech/auth/oauth/token';

    $postData = http_build_query([
        'grant_type' => 'client_credentials',
        'scope' => 'session_limited_read',
        'payment_session_id' => $sessionId
    ]);

    $ch = curl_init($authUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    curl_setopt($ch, CURLOPT_USERPWD, "$merchantId:$secretKey");
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/x-www-form-urlencoded'
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);
    return $data['access_token']; // Session-limited JWT for frontend
}

Step 3: Frontend HTML Structure#

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Checkout</title>
    <!-- HPF JavaScript Library -->
    <script src="https://pay-test.musqet.tech/assets/js/hpf/hpf-3.1.2.min.js"></script>
    <style>
        .hpf-field {
            height: 40px;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 8px;
            margin-bottom: 10px;
        }
        .hpf-field.invalid {
            border-color: red;
        }
        .hpf-field.valid {
            border-color: green;
        }
    </style>
</head>
<body>
    <form id="payment-form">
        <div class="form-group">
            <label for="pan">Card Number</label>
            <div id="pan" class="hpf-field"></div>
        </div>

        <div class="form-group">
            <label for="expiry">Expiry Date</label>
            <div id="expiry" class="hpf-field"></div>
        </div>

        <div class="form-group">
            <label for="securityCode">CVV</label>
            <div id="securityCode" class="hpf-field"></div>
        </div>

        <button type="submit" id="pay-button">Pay Now</button>
    </form>

    <script src="checkout.js"></script>
</body>
</html>

Step 4: Frontend JavaScript (checkout.js)#

// Configuration received from your backend
const config = {
    sessionId: 'SESSION_ID_FROM_BACKEND',
    hpfToken: 'HPF_JWT_TOKEN_FROM_BACKEND'
};

// Track field validity
const fieldStatus = {
    pan: false,
    expiry: false,
    securityCode: false
};

// Initialize Hosted Payment Fields
function initializeHpf() {
    // Register event listeners BEFORE initializing HPF
    // HPF dispatches events to the document, not via callback
    document.addEventListener('initialisationComplete.encodedHpf', function(event) {
        console.log('HPF initialized successfully');
    });

    document.addEventListener('fieldStatusChange.encodedHpf', function(event) {
        console.log('Field status change:', event.detail);

        const field = event.detail.field;
        // HPF uses 'state' with values: 'valid', 'invalid', 'unset'
        const isValid = event.detail.state === 'valid';

        // Map field names (HPF uses 'expiryDate', container is 'expiry')
        const fieldMapping = {
            'pan': 'pan',
            'expiryDate': 'expiry',
            'expiry': 'expiry',
            'securityCode': 'securityCode'
        };
        const mappedField = fieldMapping[field] || field;

        fieldStatus[mappedField] = isValid;

        // Update UI
        const element = document.getElementById(mappedField);
        if (element) {
            element.classList.toggle('valid', isValid);
            element.classList.toggle('invalid', event.detail.state === 'invalid');
        }

        updatePayButton();
    });

    document.addEventListener('paymentSessionSyncComplete.encodedHpf', function(event) {
        console.log('Session sync complete');
        submitPayment();
    });

    document.addEventListener('paymentSessionSyncFailed.encodedHpf', function(event) {
        console.error('Session sync failed:', event.detail);
        alert('Payment failed. Please try again.');
    });

    // Now initialize HPF
    HostedPaymentFields.initialise(config.hpfToken, {
        sessionId: config.sessionId,
        form: {
            id: 'payment-form',
            fields: {
                pan: {
                    id: 'pan',
                    placeholder: '4444 3333 2222 1111'
                },
                expiry: {
                    id: 'expiry',
                    placeholder: 'MM/YY'
                },
                securityCode: {
                    id: 'securityCode',
                    placeholder: '123'
                }
            }
        }
    });
}

// Check if all fields are valid
function updatePayButton() {
    const allValid = fieldStatus.pan && fieldStatus.expiry && fieldStatus.securityCode;
    document.getElementById('pay-button').disabled = !allValid;
}

// Handle form submission
document.getElementById('payment-form').addEventListener('submit', function(e) {
    e.preventDefault();

    // Check all fields are valid
    if (!fieldStatus.pan || !fieldStatus.expiry || !fieldStatus.securityCode) {
        alert('Please fill in all card details correctly.');
        return;
    }

    // Trigger session sync
    const syncEvent = new CustomEvent('paymentSessionSyncRequest.encodedHpf');
    document.getElementById('payment-form').dispatchEvent(syncEvent);
});

// Submit payment to your backend
async function submitPayment() {
    const response = await fetch('/api/payment/process', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            sessionId: config.sessionId,
            orderId: 'YOUR_ORDER_ID'
        })
    });

    const result = await response.json();

    if (result.success) {
        window.location.href = '/order/success';
    } else {
        alert('Payment failed: ' + result.message);
    }
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', initializeHpf);

Step 5: Process Transaction (Backend)#

// Backend endpoint: /api/payment/process
function processPayment($sessionId, $orderId, $amount, $currency = 'GBP') {
    $accessToken = createAccessToken($merchantId, $secretKey);

    $apiUrl = 'https://pay-test.musqet.tech/api/v1/transactions';

    $payload = [
        'object' => 'transaction.request',
        'ref' => 'order_' . $orderId . '_' . time(),
        'action' => 'pay', // or 'authorise' for auth-only
        'currency' => $currency,
        'amount' => (float)$amount,
        'platformType' => 'ecom',
        'source' => [
            'object' => 'source',
            'session' => [
                'object' => 'session',
                'id' => $sessionId
            ]
        ],
        'customer' => [
            'object' => 'customer',
            'ref' => 'customer_' . $orderId,
            'forename' => $billing['firstName'],
            'surname' => $billing['lastName'],
            'contact' => [
                'object' => 'contact',
                'address' => [
                    'object' => 'address',
                    'line1' => $billing['address1'],
                    'country' => $billing['countryCode'],
                    'postcode' => $billing['postcode']
                ]
            ]
        ]
    ];

    $ch = curl_init($apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $accessToken,
        'Content-Type: application/json'
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    $result = json_decode($response, true);

    // Check transaction status
    if (isset($result['id']) && in_array($result['status'], ['succeeded', 'paid', 'processed'])) {
        return ['success' => true, 'transactionId' => $result['id']];
    }

    return ['success' => false, 'message' => $result['message'] ?? 'Payment failed'];
}

Google Pay Integration (Encoded)#

Google Pay can be integrated with the Encoded gateway using either the native Google Pay JS API or Encoded's APM JavaScript library. The native approach (shown here) gives you more control. See the Google Pay Setup Guide for prerequisites.

Configuration#

Setting Description
gateway Set to 'encoded' in the Google Pay tokenization specification
gatewayMerchantId Your Encoded/Musqet Client ID (merchant_id)
Supported networks AMEX, MASTERCARD, VISA
Auth methods PAN_ONLY, CRYPTOGRAM_3DS

Frontend: Native Google Pay API#

<script src="https://pay.google.com/gp/p/js/pay.js"></script>
<script>
var tokenizationSpec = {
    type: 'PAYMENT_GATEWAY',
    parameters: {
        gateway: 'encoded',
        gatewayMerchantId: 'YOUR_ENCODED_CLIENT_ID'
    }
};

var allowedCardNetworks = ['AMEX', 'MASTERCARD', 'VISA'];
var allowedCardAuthMethods = ['PAN_ONLY', 'CRYPTOGRAM_3DS'];

var paymentsClient = new google.payments.api.PaymentsClient({
    environment: 'TEST'  // Change to 'PRODUCTION' for live
});

// After isReadyToPay() confirms support, handle the button click:
var paymentDataRequest = {
    apiVersion: 2, apiVersionMinor: 0,
    allowedPaymentMethods: [{
        type: 'CARD',
        parameters: { allowedAuthMethods: allowedCardAuthMethods, allowedCardNetworks: allowedCardNetworks },
        tokenizationSpecification: tokenizationSpec
    }],
    transactionInfo: { totalPriceStatus: 'FINAL', totalPrice: '10.00', currencyCode: 'GBP', countryCode: 'GB' },
    merchantInfo: { merchantId: 'YOUR_GOOGLE_PAY_MERCHANT_ID', merchantName: 'Your Store' }
};

var paymentData = await paymentsClient.loadPaymentData(paymentDataRequest);
var token = paymentData.paymentMethodData.tokenizationData.token;
// Send token to your backend
</script>

Backend: Submit Transaction#

The Google Pay token must be base64-encoded before sending to the Encoded API:

$token = $_POST['token'];  // Raw token string from frontend
$base64Token = base64_encode($token);

$payload = [
    'object'   => 'transaction.request',
    'action'   => 'pay',
    'ref'      => 'gpay_' . $orderId,
    'amount'   => 10.00,
    'currency' => 'GBP',
    'source'   => [
        'object'     => 'source',
        'google_pay' => [
            'object' => 'google_pay',
            'token'  => $base64Token,
        ],
    ],
];

// POST to /api/v1/transactions with Bearer token auth

If PAN_ONLY is used as an authentication method, you may receive a 3DS challenge response. See the EMV 3-D Secure section for handling.


Apple Pay Integration (Encoded)#

Apple Pay can be integrated with the Encoded gateway using either the native ApplePaySession API or Encoded's APM JavaScript library. See the Apple Pay Setup Guide for prerequisites.

Important: For Encoded, the Payment Processing Certificate is created through the Encoded API rather than directly in the Apple Developer portal. This is a three-step process managed by Encoded.

Encoded-Specific Setup: Payment Processing Certificate#

Unlike the generic Apple Pay setup, Encoded generates the Certificate Signing Request (CSR) for you:

  1. Generate CSR from Encoded:

    POST /applepay/signing-requests
    Host: pay-test.musqet.tech (or pay.musqet.tech)
    Authorization: Bearer {access_token}
    Content-Type: application/json
    
    { "merchantIdentifier": "merchant.com.yourcompany.store" }
    

    Response includes a csr field and a links.self URL.

  2. Create certificate in Apple Developer portal using the CSR from step 1 (upload the CSR content as a .cer file)

  3. Upload certificate back to Encoded:

    POST /applepay/certificates/{id}
    Host: pay-test.musqet.tech
    Authorization: Bearer {access_token}
    Content-Type: application/json
    
    { "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" }
    

You still need to separately create a Merchant Identity Certificate and verify your domain as described in the Apple Pay Setup Guide.

Configuration#

Setting Description
Supported networks amex, discover, masterCard, visa
Merchant capabilities supports3DS
Token encoding Base64 (same as Google Pay)

Frontend: Native ApplePaySession API#

if (window.ApplePaySession && ApplePaySession.canMakePayments()) {
    // Show Apple Pay button
}

function onApplePayClicked() {
    var request = {
        countryCode: 'GB',
        currencyCode: 'GBP',
        supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],
        merchantCapabilities: ['supports3DS'],
        total: { label: 'Your Store', amount: '10.00' }
    };

    var session = new ApplePaySession(3, request);

    session.onvalidatemerchant = function(event) {
        // Send event.validationURL to your backend
        // Backend calls Apple with Merchant Identity Certificate (mTLS)
        // Return merchant session to completeMerchantValidation()
    };

    session.onpaymentauthorized = function(event) {
        var token = JSON.stringify(event.payment.token);
        // Send token to your backend for processing
    };

    session.begin();
}

Backend: Submit Transaction#

The Apple Pay token must be base64-encoded before sending to the Encoded API:

$token = $_POST['token'];  // JSON-stringified ApplePayPayment.token
$base64Token = base64_encode($token);

$payload = [
    'object'   => 'transaction.request',
    'action'   => 'pay',
    'ref'      => 'apay_' . $orderId,
    'amount'   => 10.00,
    'currency' => 'GBP',
    'source'   => [
        'object'    => 'source',
        'apple_pay' => [
            'object' => 'apple_pay',
            'token'  => $base64Token,
        ],
    ],
];

// POST to /api/v1/transactions with Bearer token auth

Alternative: Encoded APM JavaScript Library#

Instead of using the native APIs directly, you can use Encoded's unified APM library which handles both Apple Pay and Google Pay:

AlternativePaymentMethods.initialise({
    jwt: APM_JWT_TOKEN,  // From createAccessToken with scope='apm'
    enablement: { applePay: true, googlePay: true },
    merchantConfig: {
        applePayMerchantId: 'merchant.com.yourcompany.store',
        displayName: 'Your Store'
    },
    payment: { amount: '10.00', currency: 'GBP', countryCode: 'GB' },
    domContainers: {
        applePay: document.getElementById('applePay-container'),
        googlePay: document.getElementById('googlePay-container')
    },
    applePayListeners: {
        paymentSheetSuccessful: function(event) { /* event.token */ }
    },
    googlePayListeners: {
        paymentSheetSuccessful: function(event) { /* event.token */ }
    }
});

The APM library is available at https://pay-test.musqet.tech (sandbox) / pay.musqet.tech (production)/assets/js/apm/apm-1.0.0.min.js and requires an APM-scoped JWT (see Authentication for APMs).


Transaction API#

Transaction Actions#

Action Description
pay Authorise and capture in a single request
authorise Authorise only (capture later)
capture Capture a previous authorisation
refund Refund a captured transaction
void Cancel an authorisation

Transaction Request Structure#

{
    "object": "transaction.request",
    "ref": "unique_reference",
    "action": "pay",
    "currency": "GBP",
    "amount": 99.99,
    "platformType": "ecom",
    "source": {
        "object": "source",
        "session": {
            "object": "session",
            "id": "session-uuid"
        }
    },
    "customer": {
        "object": "customer",
        "ref": "customer_id",
        "forename": "John",
        "surname": "Doe",
        "email": "john@example.com",
        "contact": {
            "object": "contact",
            "address": {
                "object": "address",
                "line1": "123 Main St",
                "line2": "",
                "line3": "London",
                "country": "GBR",
                "postcode": "SW1A 1AA"
            }
        }
    }
}

Transaction Response#

{
    "object": "transaction",
    "id": "b015dd96-a8e5-46e1-b390-af39d0b93960",
    "creationDate": "2024-01-15T10:14:20Z",
    "status": "processed",
    "request": {
        "object": "transaction.request",
        "id": "700ff349-4038-4063-ad45-715ce9d8a48c",
        "action": "pay",
        "amount": 99.99,
        "currency": "GBP"
    },
    "response": {
        "object": "transaction.response",
        "authorisationCode": "123456",
        "responseCode": "00",
        "responseMessage": "Approved"
    }
}

Webhooks / Callbacks#

Callback Payload Example#

{
    "object": "callback",
    "event": "transaction.completed",
    "data": {
        "transactionId": "b015dd96-a8e5-46e1-b390-af39d0b93960",
        "orderId": "ORDER_12345",
        "status": "succeeded",
        "amount": 99.99,
        "currency": "GBP",
        "timestamp": "2024-01-15T10:14:25Z"
    }
}

Handling Callbacks (Backend)#

// webhook.php - Endpoint receiving webhooks from Musqet gateway
$requestBody = file_get_contents('php://input');
$payload = json_decode($requestBody, true);

$event = $payload['event'] ?? null;
$data = $payload['data'] ?? [];

// Log for debugging
error_log('Payment callback received: ' . $event . ' - ' . json_encode($data));

switch ($event) {
    case 'transaction.completed':
        if ($data['status'] === 'succeeded') {
            // Mark order as paid
            updateOrderStatus($data['orderId'], 'paid', $data['transactionId']);
        } else {
            updateOrderStatus($data['orderId'], 'failed');
        }
        break;

    case 'transaction.refunded':
        updateOrderStatus($data['orderId'], 'refunded');
        break;
}

// Always respond with 200 to acknowledge receipt
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);

Test Cards#

Use these cards in sandbox/test mode:

Card Number Expiry CVV Result
4444 3333 2222 1111 Any future date Any 3 digits Approved
4000 0000 0000 0002 Any future date Any 3 digits Declined
4000 0000 0000 3220 Any future date Any 3 digits 3D Secure Required

Country Codes#

The Encoded API uses ISO 3166-1 alpha-3 country codes:

Country ISO2 ISO3
United Kingdom GB GBR
France FR FRA
Germany DE DEU
United States US USA
Spain ES ESP
Italy IT ITA

Error Handling#

Common Error Responses#

{
    "object": "error",
    "code": "invalid_request",
    "message": "The request was invalid",
    "details": [
        {
            "field": "amount",
            "message": "Amount must be greater than 0"
        }
    ]
}

Error Codes#

Code Description
invalid_request Request validation failed
authentication_failed Invalid credentials
session_expired Payment session has expired
declined Transaction was declined
insufficient_funds Card has insufficient funds
3ds_required 3D Secure authentication required

Running the Encoded Examples#

Security Note: The example API endpoints use Access-Control-Allow-Origin: * for development convenience. In production, restrict this to your specific domain(s) or remove it entirely if your frontend and API are on the same origin.

cd Documentation/encoded-gateway-examples
php -S localhost:8080

Open http://localhost:8080 — the index.html redirects to the card checkout page. Use the Configure page to enter your credentials (or copy config.php to config.local.php and edit manually).

Note: The embedded card checkout (checkout-card.html) requires valid Encoded credentials because it makes server-side OAuth calls during initialization. If credentials are not configured, it will display a clear error message instead of attempting API calls.

URL Description
http://localhost:8080/frontend/checkout-card.html Card payment (Hosted Payment Fields)
http://localhost:8080/frontend/checkout-hosted.html Card payment (redirect / hosted payment page)
http://localhost:8080/frontend/configure.html Configuration page

Encoded Example Files#

encoded-gateway-examples/
├── index.html                     - Redirects to /frontend/checkout-card.html
├── config.php                     - Config template
├── config.local.php               - Your credentials (gitignored)
├── EncodedApi.php                 - Core API class (OAuth, sessions, transactions, orders)
├── redirect-mode.php              - Redirect/HPP standalone demo
├── api/
│   ├── config.php                 - Config read/write API (for configure page)
│   ├── hpf-session.php            - Creates session + HPF token for embedded checkout
│   ├── process-payment.php        - Processes card payment via session ID
│   ├── build-redirect.php         - Creates order and returns HPP URL
│   └── webhook.php                - Callback handler
└── frontend/
    ├── checkout-card.html         - Card checkout (Hosted Payment Fields)
    ├── checkout-hosted.html       - Card checkout (redirect / hosted payment page)
    ├── configure.html             - Web UI for editing config.local.php
    ├── success.html               - Payment success page
    └── cancel.html                - Payment cancellation page

Bitcoin Payments#

Bitcoin payments use the Musqet Checkout API at api.musqet.tech. The Bitcoin payment flow is completely independent of the card processing gateway (Cardstream or Encoded) and does not require any gateway credentials or signature authentication — only a Musqet API key.

Bitcoin examples are in their own standalone directory: bitcoin-examples/.

Three checkout modes are supported:

  1. DIY — Your own UI, talking directly to the Musqet REST API. Gives you complete control over the look and feel — render your own QR codes, build your own payment modal, style it to match your brand. Requires more code but no external dependencies beyond the API.
  2. iframe — Musqet's pre-built payment component embedded in an iframe on your page, communicating via postMessage. Quick to implement — just point an iframe at a URL and listen for events.
  3. redirect — Full-page redirect to the Musqet payment component, with server-side verification on return. The simplest integration — just redirect and handle the callback.

The iframe and redirect modes use the hosted payment component at bitcoin-payment-component.musqet.tech and require minimal code. The DIY mode bypasses the hosted component entirely, using the REST API to create checkouts and retrieve Lightning invoices and on-chain addresses directly.


Bitcoin Environments#

Service Production Sandbox
Payment component https://bitcoin-payment-component.musqet.tech https://staging.bitcoin-payment-component.musqet.tech
Get Checkout API https://api.musqet.tech https://staging.api.musqet.tech

Bitcoin Configuration#

Key Type Description
bitcoin_business_id string Your Musqet Bitcoin Business ID
bitcoin_sandbox bool Use sandbox environment (true for staging)
musqet_api_key string API key for verifying checkouts via the Get Checkout API
currency string Default currency (GBP, USD, EUR)

Generating the Bitcoin URL (Backend)#

The Bitcoin component URL is constructed from your Business ID and order details:

function getBitcoinUrl($businessId, $orderId, $amount, $currency, $successUrl, $cancelUrl, $isSandbox = true) {
    $baseUrl = $isSandbox
        ? 'https://staging.bitcoin-payment-component.musqet.tech'
        : 'https://bitcoin-payment-component.musqet.tech';

    // Fiat currencies: minor units (pence/cents). BTC: satoshis (no conversion).
    $amountInMinor = strtoupper($currency) === 'BTC' ? (int)$amount : (int)($amount * 100);

    return sprintf(
        '%s/b/%s/c?amount=%d&successUrl=%s&cancelUrl=%s&currency=%s&orderId=%s',
        $baseUrl,
        $businessId,
        $amountInMinor,
        urlencode($successUrl),
        urlencode($cancelUrl),
        $currency,
        urlencode($orderId)
    );
}

URL Parameters#

Parameter Description
businessId Your Musqet Bitcoin Business ID (path segment)
amount Amount in minor units (pence/cents for fiat, satoshis for BTC)
currency Alpha currency code (GBP, USD, EUR, BTC)
successUrl URL the component redirects to on successful payment
cancelUrl URL the component redirects to on cancellation
orderId Your order reference

DIY Mode#

In DIY mode, you build your own payment UI and talk directly to the Musqet REST API. No iframe, no hosted component, no external JavaScript — just API calls and your own frontend code. This gives you complete control over the user experience: render QR codes in your own style, build a custom modal or inline payment form, and match your brand exactly.

API Endpoints#

Endpoint Method Description
/api/v1/checkouts POST Create a new checkout (returns checkout ID)
/api/v1/checkouts/{id} GET Get checkout details including Lightning invoice and on-chain address
/api/v1/checkouts/{id}/subscribe GET Server-Sent Events stream for real-time status updates

All endpoints require a Bearer token (your musqet_api_key).

Flow#

  1. Backend creates checkout via POST /api/v1/checkouts with businessId, amountInCents (fiat) or satsAmount (BTC), currency, externalId, and paymentMethods
  2. Backend fetches full checkout details via GET /api/v1/checkouts/{id} — response includes whichever payment methods the merchant has enabled (e.g. lightningInvoice, onchainInvoice, cashuInvoice)
  3. Frontend displays QR codes for whichever methods are available — show tabs if more than one
  4. Frontend polls GET /api/v1/checkouts/{id} (or connects to the SSE endpoint) to detect when status changes to COMPLETE
  5. On completion, verify the checkout server-side via GET /api/v1/checkouts/{id} — check that externalId and businessId match your records
  6. If verified, redirect to your success page. If verification fails, redirect to a failure page — in a real integration, pass the failure back to your ecommerce store to handle

Creating a Checkout (Backend)#

$isSandbox = true;
$apiHost = $isSandbox ? 'staging.api.musqet.tech' : 'api.musqet.tech';

$payload = [
    'businessId'     => $config['bitcoin_business_id'],
    'externalId'     => $orderId,
    'currency'       => 'GBP',
    'amountInCents'  => 100,  // £1.00
    'paymentMethods' => ['LIGHTNING', 'ON_CHAIN', 'CASHU'],
];

$ch = curl_init('https://' . $apiHost . '/api/v1/checkouts');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $config['musqet_api_key'],
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$checkoutId = $response['id'];

Fetching Payment Details (Backend)#

$ch = curl_init('https://' . $apiHost . '/api/v1/checkouts/' . $checkoutId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $config['musqet_api_key'],
    ],
]);
$checkout = json_decode(curl_exec($ch), true);

$tx = $checkout['transactions'][0];
$satsAmount = $tx['amount'];  // Amount in satoshis

// Each invoice field is populated or null depending on what the merchant has enabled
$lightningInvoice = $tx['lightningInvoice']['paymentRequest'] ?? null;  // BOLT11 string
$onchainAddress   = $tx['onchainInvoice']['address'] ?? null;           // Bitcoin address
$cashuToken       = $tx['cashuInvoice']['token'] ?? null;               // Cashu eCash token

Rendering QR Codes (Frontend)#

Use any QR code library (the example uses qrcode.js from CDN). For Lightning, encode the BOLT11 invoice in uppercase for more efficient QR encoding. For on-chain, use a BIP21 URI:

// Lightning — uppercase BOLT11 for smaller QR (alphanumeric mode)
QRCode.toCanvas(canvas, invoice.toUpperCase(), { width: 250 });

// On-chain — BIP21 URI with amount in BTC
var btcAmount = (satsAmount / 1e8).toFixed(8);
QRCode.toCanvas(canvas, 'bitcoin:' + address + '?amount=' + btcAmount, { width: 250 });

Polling for Payment Status (Frontend)#

var poll = setInterval(async function() {
    var resp = await fetch('/api/bitcoin-checkout-status.php?checkout_id=' + checkoutId);
    var data = await resp.json();
    if (data.checkout.status === 'COMPLETE') {
        clearInterval(poll);
        // Payment received — verify server-side before redirecting
        var verify = await fetch('/api/verify-checkout.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ checkout_id: checkoutId, order_id: orderId })
        });
        var result = await verify.json();
        if (result.success && result.verified) {
            window.location.href = 'success.html?order_id=' + orderId;
        } else {
            window.location.href = 'failed.html?reason=' + encodeURIComponent(result.error || 'Verification failed');
        }
    }
}, 3000);

Checkout Response Fields#

Field Description
id Checkout ID
status OPEN, COMPLETE, EXPIRED, CANCELLED
amount Amount (cents for fiat, sats for BTC)
currency Currency code
externalId Your order reference
transactions[].amount Amount in satoshis
transactions[].lightningInvoice.paymentRequest BOLT11 invoice string (null if not enabled)
transactions[].onchainInvoice.address Bitcoin on-chain address (null if not enabled)
transactions[].cashuInvoice.token Cashu eCash token (null if not enabled)

iframe Mode#

In iframe mode, the Musqet payment component is embedded in an <iframe> on your page. This is quick to implement — just load a URL in an iframe and listen for postMessage events. The look and feel is controlled by the hosted component. The successUrl and cancelUrl point to small PHP pages that extract the checkoutId from the URL and relay it to the parent window via postMessage. The parent then verifies the checkout client-side before redirecting.

Flow#

  1. Backend calls bitcoin-session.php → returns bitcoin_url
  2. Frontend loads bitcoin_url + '&iframe=true' in an iframe
  3. User pays via Lightning or on-chain
  4. Component redirects iframe to successUrl with checkoutId appended
  5. bitcoin-success.php extracts checkoutId and posts musqet-bitcoin-payment-complete to parent via postMessage
  6. Parent calls verify-checkout.php to verify the checkout server-side
  7. On success, parent redirects to success.html

Frontend (postMessage listener)#

window.addEventListener('message', function(event) {
    var data = event.data;
    if (typeof data === 'string') { try { data = JSON.parse(data); } catch(e) { return; } }
    if (!data || typeof data !== 'object') return;

    if (data.type === 'musqet-bitcoin-payment-complete') {
        // data.checkoutId — use this to verify the payment server-side
        verifyAndRedirect(data.checkoutId);
    } else if (data.type === 'musqet-bitcoin-payment-cancelled') {
        window.location.href = 'cancel.html';
    } else if (data.type === 'musqet-bitcoin-payment-error') {
        console.error('Bitcoin payment error:', data.error);
    }
});

postMessage Events#

Event Type Description Data Fields
musqet-bitcoin-payment-complete Payment confirmed checkoutId, orderId, status
musqet-bitcoin-payment-cancelled Customer cancelled orderId
musqet-bitcoin-payment-error Payment failed error (string)

redirect Mode#

In redirect mode, the browser navigates directly to the Musqet payment component (no iframe). This is the simplest integration — just redirect the user and handle the return. The successUrl and cancelUrl point to bitcoin-redirect-return.php, which verifies the checkout server-side before redirecting the user to the success or cancel page.

Flow#

  1. User clicks "Pay with Bitcoin" → frontend calls bitcoin-session.php?mode=redirect → returns bitcoin_url
  2. Frontend redirects browser: window.location.href = bitcoin_url
  3. User pays via Lightning or on-chain
  4. Component redirects browser to bitcoin-redirect-return.php?status=success&checkoutId=...
  5. bitcoin-redirect-return.php verifies the checkout via the Get Checkout API, notifies the webhook, and redirects to success.html

Verifying a Bitcoin Checkout (Get Checkout API)#

After a Bitcoin payment completes, the checkoutId returned by the component should be verified server-side to prevent spoofing. This is done by calling the Musqet Get Checkout API:

GET https://[staging.]api.musqet.tech/api/v1/checkouts/{checkoutId}
Authorization: Bearer {musqet_api_key}
Accept: application/json

The response includes the checkout details (id, externalId, businessId, status, etc.). Verify that:

This verification is performed by verify-checkout.php (iframe mode, called from frontend) and bitcoin-redirect-return.php (redirect mode, called server-side).


Running the Bitcoin Examples#

cd Documentation/bitcoin-examples
php -S localhost:8080

Open http://localhost:8080 — the index.html redirects to the DIY checkout page. Use the Configure page to enter your Bitcoin Business ID, API key, and sandbox settings.

URL Description
http://localhost:8080/frontend/checkout-bitcoin-modal.html Bitcoin checkout (DIY mode)
http://localhost:8080/frontend/checkout-bitcoin-iframe.html Bitcoin checkout (iframe mode)
http://localhost:8080/frontend/checkout-bitcoin-redirect.html Bitcoin checkout (redirect mode)
http://localhost:8080/frontend/configure.html Configuration page

Bitcoin Example Files#

bitcoin-examples/
├── index.html                     - Redirects to /frontend/checkout-bitcoin-modal.html
├── config.php                     - Config template
├── config.local.php               - Your credentials (gitignored)
├── BitcoinApi.php                 - Helper class for generating Bitcoin component URLs
├── api/
│   ├── config.php                 - Config read/write API (for configure page)
│   ├── bitcoin-create-checkout.php - DIY: creates checkout via Musqet REST API
│   ├── bitcoin-checkout-status.php - DIY: polls checkout status via Musqet REST API
│   ├── bitcoin-session.php        - Returns Bitcoin component URL (supports mode=iframe|redirect)
│   ├── verify-checkout.php        - Verifies checkout via Get Checkout API
│   ├── bitcoin-redirect-return.php - Redirect return handler (verifies + redirects to success/cancel)
│   ├── bitcoin-success.php        - iframe success page (postMessage to parent)
│   ├── bitcoin-cancel.php         - iframe cancel page (postMessage to parent)
│   └── webhook.php                - Webhook receiver (logs events)
└── frontend/
    ├── checkout-bitcoin-modal.html     - Bitcoin checkout (DIY mode — custom modal via REST API)
    ├── checkout-bitcoin-iframe.html    - Bitcoin checkout (iframe mode)
    ├── checkout-bitcoin-redirect.html  - Bitcoin checkout (redirect mode)
    ├── configure.html             - Web UI for editing config.local.php
    ├── success.html               - Payment success page
    ├── failed.html                - Payment verification failed page
    └── cancel.html                - Payment cancellation page


Apple Pay Setup Guide#

Apple Pay on the Web allows customers to pay using cards stored in their Apple Wallet. Setting it up requires several steps across the Apple Developer portal, your server, and your domain. This section is gateway-agnostic — the steps apply regardless of whether you use Cardstream, Encoded, or any other payment processor.

Step 1: Create an Apple Developer Account#

If you don't already have one, enrol at developer.apple.com/programs. You need a paid Apple Developer Program membership (US $99/year). Apple Pay merchant configuration is only available to Account Holders and Admins.

Step 2: Create a Merchant Identifier#

A Merchant ID uniquely identifies your business to Apple Pay. It can be reused across multiple apps and websites and never expires.

  1. Sign in to Certificates, Identifiers & Profiles
  2. In the sidebar, click Identifiers, then click the + button
  3. Select Merchant IDs and click Continue
  4. Enter a description (e.g. "My Store Apple Pay") and an identifier (e.g. merchant.com.yourcompany.store)
  5. Click Continue, then Register

Step 3: Create a Payment Processing Certificate#

This certificate encrypts payment data between Apple and your payment processor. Your processor may handle this for you — check their documentation. If you need to create one yourself:

  1. Go to IdentifiersMerchant IDs → select your merchant ID
  2. Under Apple Pay Payment Processing Certificate, click Create Certificate
  3. Follow the prompts to upload a Certificate Signing Request (CSR) — you can generate one using Keychain Access on macOS:
    • Open Keychain AccessCertificate AssistantRequest a Certificate From a Certificate Authority
    • Enter your email, select Saved to disk, click Continue
  4. Upload the .certSigningRequest file, click Continue
  5. Download the .cer file

Step 4: Create a Merchant Identity Certificate#

This certificate authenticates your server when it communicates with Apple's servers during merchant validation. This is separate from the Payment Processing Certificate.

  1. Go to IdentifiersMerchant IDs → select your merchant ID
  2. Under Apple Pay Merchant Identity Certificate, click Create Certificate
  3. Create another CSR (or reuse the one from Step 3) and upload it
  4. Download the .cer file
  5. Convert to PEM format for use in your server code:
    # Convert .cer to .pem (certificate)
    openssl x509 -inform DER -in merchant_id.cer -out merchant_id_cert.pem
    
    # Export private key from Keychain (on the Mac where you created the CSR):
    # In Keychain Access, find the private key associated with the cert,
    # right-click → Export → save as .p12
    # Then convert to PEM:
    openssl pkcs12 -in merchant_id.p12 -out merchant_id_key.pem -nodes -nocerts
    

You will need both merchant_id_cert.pem and merchant_id_key.pem for merchant validation.

Step 5: Register and Verify Your Domain#

Apple needs to verify that you own the domain where Apple Pay will be used.

  1. Go to IdentifiersMerchant IDs → select your merchant ID
  2. Under Merchant Domains, click Add Domain
  3. Enter your fully qualified domain name (e.g. www.yourstore.com) and click Save
  4. Click Download to get the domain verification file
  5. Host the file at: https://www.yourstore.com/.well-known/apple-developer-merchantid-domain-association

Important requirements for the verification file:

  1. Once the file is hosted, go back to the Apple Developer portal and click Verify

If verification fails, common causes are:

Step 6: Server Requirements#

Your server must meet these requirements for Apple Pay to work:

Requirement Detail
HTTPS Apple Pay only works on secure origins. Your checkout page must be served over HTTPS.
TLS version TLS 1.2 or later is required for all Apple Pay communication
Certificate Signed with SHA-256 or stronger, minimum 2048-bit RSA key or 256-bit ECC key
Forward secrecy Required — use ECDHE cipher suites

Recommended cipher suites: ECDHE_ECDSA_AES and ECDHE_RSA_AES in GCM mode.

Step 7: How Merchant Validation Works#

When a customer initiates an Apple Pay payment, Apple requires your server to prove it is a legitimate merchant. This happens via a merchant validation session — a server-to-server call using mutual TLS (mTLS).

The flow:#

  1. Customer clicks Apple Pay button → your JS creates an ApplePaySession
  2. Safari fires the onvalidatemerchant event with a validationURL (an Apple URL)
  3. Your frontend sends this validationURL to your backend
  4. Your backend POSTs to the validationURL using the Merchant Identity Certificate (mTLS) with this JSON body:
    {
      "merchantIdentifier": "merchant.com.yourcompany.store",
      "displayName": "Your Store Name",
      "initiative": "web",
      "initiativeContext": "www.yourstore.com"
    }
    
  5. Apple returns a merchant session object (opaque JSON)
  6. Your backend returns this session to the frontend
  7. Your frontend calls session.completeMerchantValidation(merchantSession)
  8. The Apple Pay payment sheet appears

Merchant validation endpoints:#

Region URL
Global https://apple-pay-gateway.apple.com/paymentservices/paymentSession
China https://cn-apple-pay-gateway.apple.com/paymentservices/paymentSession

Note: The validationURL provided in the onvalidatemerchant event should be used instead of hardcoding the URL above, as Apple may provide different URLs in different contexts.

PHP example (merchant validation):#

$validationUrl = $_POST['validationURL'];  // From frontend
$certPath = '/path/to/merchant_id_cert.pem';
$keyPath  = '/path/to/merchant_id_key.pem';

$payload = json_encode([
    'merchantIdentifier' => 'merchant.com.yourcompany.store',
    'displayName'        => 'Your Store',
    'initiative'         => 'web',
    'initiativeContext'   => 'www.yourstore.com',
]);

$ch = curl_init($validationUrl);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_SSLCERT        => $certPath,
    CURLOPT_SSLKEY         => $keyPath,
    CURLOPT_TIMEOUT        => 15,
]);

$merchantSession = curl_exec($ch);
// Return $merchantSession to the frontend as-is

Browser Support#

Browser Support
Safari (macOS, iOS) Full support via ApplePaySession API
Chrome (iOS 16+) Supported via Payment Request API
Other browsers Not supported

Testing Apple Pay#

Unlike Google Pay, Apple Pay does not have a simple "test mode" flag. Testing is done through Apple's Sandbox environment using dedicated test accounts.

Step 1: Create a Sandbox Tester Account#

  1. Sign in to App Store Connect
  2. Go to Users and AccessSandboxTesters
  3. Click + to create a new sandbox tester
  4. Fill in the required details (name, email, password) — this does not need to be a real email address, but you must remember the credentials

Step 2: Sign In on Your Test Device#

Apple automatically provisions sandbox test cards for the tester account — you do not need to add real payment cards.

Step 3: Test the Payment Flow#

  1. Make sure your payment gateway is pointing to its sandbox/test environment (e.g. pay-test.musqet.tech for Encoded, or your Cardstream test credentials)
  2. Open your checkout page in Safari on the device signed into the sandbox account
  3. Click the Apple Pay button — the payment sheet will appear with the sandbox test cards
  4. Authorise the payment — the token will be sent to your sandbox gateway, which will process it without real charges

Important Notes#


Google Pay Setup Guide#

Google Pay on the Web allows customers to pay using cards stored in their Google account. The setup process involves the Google Pay & Wallet Console and is significantly simpler than Apple Pay. This section is gateway-agnostic.

Step 1: Build Your Integration#

No account setup or approval is needed to start building. Google Pay can be developed and tested immediately using TEST mode.

Supported browsers: Chrome, Firefox, Safari, Edge, Opera, UC Browser.

Requirements: Your checkout page must be served over HTTPS with a TLS domain-validated certificate (even in test mode, some browsers require HTTPS).

The core integration involves:

  1. Load the library: <script src="https://pay.google.com/gp/p/js/pay.js"></script>
  2. Create a PaymentsClient with environment: 'TEST'
  3. Check readiness: Call isReadyToPay() — only show the button if this returns true
  4. Render the button: Use Google's pre-built button component
  5. Handle payment: Build a PaymentDataRequest with your merchant info, accepted card networks, and transaction amount
  6. Extract the token: The response contains paymentMethodData.tokenizationData.token — send this to your backend for processing

Tokenization specification:#

For gateway integrations (most common), configure:

{
    type: 'PAYMENT_GATEWAY',
    parameters: {
        gateway: 'your-gateway-name',        // e.g. 'crst' for Cardstream
        gatewayMerchantId: 'YOUR_MERCHANT_ID' // Your merchant ID with the gateway
    }
}

Step 2: Test Your Integration#

With environment: 'TEST' set, you can test the full payment flow immediately:

Important: Google Pay TEST mode returns dummy tokens with sample cryptographic data. These are sufficient to verify your frontend integration works correctly (button renders, payment sheet opens, token is received and sent to your backend). However, your payment gateway's sandbox may not be able to decrypt or process these dummy tokens — a gateway error at this stage is expected and does not indicate a problem with your integration. End-to-end payment processing can only be fully tested in PRODUCTION mode with a real Google Pay Merchant ID.

Step 3: Create a Google Business Profile#

Once your integration is working in test mode and you're ready to go live:

  1. Go to the Google Pay & Wallet Console
  2. Sign in with a Google account (or create one)
  3. Select Merchant as your business type
  4. Complete the required business information
  5. Accept the Terms of Service and Acceptable Use Policy

Your Merchant ID will appear in the top-right corner of the console after setup. You'll need this for production.

Step 4: Submit for Production Approval#

  1. In the Google Pay & Wallet Console, go to Google Pay APIIntegrations
  2. Click Integrate with your website
  3. Click Add website and enter your top-level domain
  4. Select your integration type:
    • Gateway — if you use a payment service provider (most common)
    • Direct — if you are PCI DSS compliant and handle card data directly
  5. Upload screenshots of your checkout page showing the Google Pay button and payment flow
  6. Click Submit for review

Google's team will review your integration. This typically takes a few business days.

Step 5: Go Live#

Once approved:

  1. Update your PaymentsClient to use environment: 'PRODUCTION'
  2. Set merchantInfo.merchantId to your actual Merchant ID from the console
  3. Set merchantInfo.merchantName to your business name
  4. Deploy to production
const paymentsClient = new google.payments.api.PaymentsClient({
    environment: 'PRODUCTION',
    merchantInfo: {
        merchantId: 'BCR2DN4T...', // Your real Merchant ID
        merchantName: 'Your Store Name'
    }
});

Google Pay Brand Guidelines#

Key requirements:

Key Differences: Google Pay vs Apple Pay#

Aspect Google Pay Apple Pay
Setup complexity Simple — console + screenshots Complex — certificates, domain verification, mTLS
Test mode Free, no approval needed Requires sandbox Apple Developer account
Approval process Submit screenshots, ~days Manual domain verification + certificate management
Certificate management None Payment Processing Certificate (25-month expiry) + Merchant Identity Certificate
Domain verification Not required Required — host verification file
Browser support All major browsers Safari only (+ Chrome on iOS 16+)
Server requirements Standard HTTPS HTTPS + TLS 1.2 + forward secrecy + mTLS for validation
Merchant validation None Required on every payment session

Changelog#