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:
- Redirect Mode - Customer is redirected to Cardstream's hosted payment page
- Embedded Mode - Cardstream's hosted fields are embedded directly in your checkout page (requires jQuery)
- Google Pay - Native Google Pay button using Google's Pay API, processed via Cardstream's
/direct/endpoint - Apple Pay - Native Apple Pay button using Safari's ApplePaySession API, processed via Cardstream's
/direct/endpoint
Table of Contents (Cardstream)#
- Gateway URL & IP Whitelisting
- Authentication (Signature)
- Redirect Mode
- Embedded Mode (Hosted Fields)
- Google Pay
- Apple Pay
- 3D Secure
- Refunds
- Webhooks / Callbacks
- Numeric Code Formats
- Integration Testing
- 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:
- Embedded card payments (processing the
paymentTokenfrom hostedfields.js) - Google Pay and Apple Pay token processing
- 3DS continuation transactions
- Refunds
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#
- Build transaction fields with your merchant ID, order details, and redirect URL
- Sign the fields with SHA-512
- Render an auto-submit HTML form POSTing to
https://payments.musqet.tech/hosted/ - Customer completes payment on the gateway's hosted page
- Gateway redirects back to your
redirectURLwith 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#
- Frontend: Load jQuery +
hostedfields.jsfrom the gateway - Frontend: Initialize the jQuery
hostedFormplugin on your form - Frontend: User enters card details into hosted field iframes
- Frontend: Call
getPaymentDetails()to get apaymentToken - Backend: Send
paymentToken+ order details tohttps://payments.musqet.tech/direct/ - Backend: Handle response (success, 3DS, or error)
Prerequisites#
- jQuery (any recent version)
- No server-side session creation needed (no OAuth required)
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#
- Frontend loads Google's Pay API script
- Create a
PaymentsClientand callisReadyToPay()to check device/browser support - Render the Google Pay button
- Customer clicks button → Google Pay sheet opens → customer authorizes
- Frontend receives
PaymentDatawith token atpaymentMethodData.tokenizationData.token - Frontend sends the raw token string to your backend
- Backend POSTs to Cardstream
/direct/withpaymentMethod=googlepay+paymentToken=<token>alongside standard transaction fields - Cardstream returns standard response (
responseCode=0for success,65802for 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#
- Frontend checks
ApplePaySession.canMakePayments()— hides option if unavailable - User clicks Apple Pay button →
new ApplePaySession(3, paymentRequest)with amount/currency onvalidatemerchant: frontend sendsvalidationURLto backend → backend POSTs to Apple with merchant cert → returns merchant session →completeMerchantValidation()onpaymentauthorized: extractevent.payment.token→ stringify → store as payment token- Backend receives token → sends to Cardstream
/direct/withpaymentMethod=applepay,paymentToken=<token> - Response:
responseCode=0success,658023DS required, else error
Prerequisites#
See the Apple Pay Setup Guide above for detailed step-by-step instructions on all of the following:
- Apple Developer account with Apple Pay merchant registration (Merchant ID)
- Payment Processing Certificate (for your payment processor)
- Merchant Identity Certificate (
.pem— for merchant validation mTLS calls) - Domain verification file hosted at
/.well-known/apple-developer-merchantid-domain-association - HTTPS with TLS 1.2+ and forward secrecy
- Safari, or Chrome on iOS 16+
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:
threeDSRedirectURL— the URL on your server where the ACS will POST results after each 3DS step- Device fields — browser/device information required for 3DS2 risk assessment (see Device Fields below)
- Do not include
redirectURLfor/direct/transactions — this causes the gateway to return 302 redirects instead of direct responses, which can fail behind CDN/WAF infrastructure
Flow#
- Initial SALE returns
responseCode=65802withthreeDSURL,threeDSRef, andthreeDSRequest - Device fingerprinting: If
threeDSRequestcontainsthreeDSMethodData, this is a silent fingerprinting step. Submit it in a hidden iframe; the ACS will POST back to yourthreeDSRedirectURL(with?threeDSAcsResponse=methodappended) - Fingerprinting continuation: Your callback sends
threeDSRef+threeDSResponseto/direct/. The gateway may return success, failure, or another65802requiring a visible challenge - Visible challenge: If the response contains
threeDSRequestwithcreq, display the ACS challenge page in an overlay/iframe targeting thethreeDSURL - Challenge continuation: After the user authenticates, the ACS POSTs
cresto yourthreeDSRedirectURL. Your callback sends a final continuation to/direct/ - 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:
responseCode=0— refund successful- Any other code — refund failed (see
responseMessagefor details)
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:
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:
- Redirect Mode - Customer is redirected to Encoded's hosted payment page
- Embedded Mode - Encoded's payment fields are embedded directly in your checkout page
Table of Contents (Encoded)#
- Environments & URLs
- Authentication
- Redirect Mode (Hosted Payment Page)
- Embedded Mode
- Transaction API
- Webhooks / Callbacks
- Test Cards
- 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:
- Merchant ID (Client ID)
- Secret Key (Client Secret)
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#
- Create an order via API
- Redirect customer to the payment URL
- Customer completes payment
- Customer is redirected back to your success/cancel URL
- 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#
- Backend: Create a payment session
- Backend: Generate a session-limited JWT token
- Frontend: Initialize HPF JavaScript library
- Frontend: User enters card details
- Frontend: Sync session (save card data to session)
- 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:
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
csrfield and alinks.selfURL.Create certificate in Apple Developer portal using the CSR from step 1 (upload the CSR content as a
.cerfile)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:
- 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.
- 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. - 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¤cy=%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#
- Backend creates checkout via
POST /api/v1/checkoutswithbusinessId,amountInCents(fiat) orsatsAmount(BTC),currency,externalId, andpaymentMethods - 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) - Frontend displays QR codes for whichever methods are available — show tabs if more than one
- Frontend polls
GET /api/v1/checkouts/{id}(or connects to the SSE endpoint) to detect whenstatuschanges toCOMPLETE - On completion, verify the checkout server-side via
GET /api/v1/checkouts/{id}— check thatexternalIdandbusinessIdmatch your records - 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#
- Backend calls
bitcoin-session.php→ returnsbitcoin_url - Frontend loads
bitcoin_url + '&iframe=true'in an iframe - User pays via Lightning or on-chain
- Component redirects iframe to
successUrlwithcheckoutIdappended bitcoin-success.phpextractscheckoutIdand postsmusqet-bitcoin-payment-completeto parent viapostMessage- Parent calls
verify-checkout.phpto verify the checkout server-side - 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#
- User clicks "Pay with Bitcoin" → frontend calls
bitcoin-session.php?mode=redirect→ returnsbitcoin_url - Frontend redirects browser:
window.location.href = bitcoin_url - User pays via Lightning or on-chain
- Component redirects browser to
bitcoin-redirect-return.php?status=success&checkoutId=... bitcoin-redirect-return.phpverifies the checkout via the Get Checkout API, notifies the webhook, and redirects tosuccess.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:
externalIdmatches your order IDbusinessIdmatches your configured Bitcoin Business ID
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.
- Sign in to Certificates, Identifiers & Profiles
- In the sidebar, click Identifiers, then click the + button
- Select Merchant IDs and click Continue
- Enter a description (e.g. "My Store Apple Pay") and an identifier (e.g.
merchant.com.yourcompany.store) - 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:
- Go to Identifiers → Merchant IDs → select your merchant ID
- Under Apple Pay Payment Processing Certificate, click Create Certificate
- Follow the prompts to upload a Certificate Signing Request (CSR) — you can generate one using Keychain Access on macOS:
- Open Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority
- Enter your email, select Saved to disk, click Continue
- Upload the
.certSigningRequestfile, click Continue - Download the
.cerfile
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.
- Go to Identifiers → Merchant IDs → select your merchant ID
- Under Apple Pay Merchant Identity Certificate, click Create Certificate
- Create another CSR (or reuse the one from Step 3) and upload it
- Download the
.cerfile - 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.
- Go to Identifiers → Merchant IDs → select your merchant ID
- Under Merchant Domains, click Add Domain
- Enter your fully qualified domain name (e.g.
www.yourstore.com) and click Save - Click Download to get the domain verification file
- Host the file at:
https://www.yourstore.com/.well-known/apple-developer-merchantid-domain-association
Important requirements for the verification file:
- The file is a binary file provided by Apple — do not modify it
- Serve it with
Content-Type: application/octet-stream(not text/html) - It must be accessible via HTTPS at the exact path above
- No redirects — Apple does not follow 3xx redirects when verifying
- The URL must not have a trailing slash or additional path components
- Once the file is hosted, go back to the Apple Developer portal and click Verify
If verification fails, common causes are:
- The file is not at the exact path
/.well-known/apple-developer-merchantid-domain-association - Your server returns a redirect instead of the file directly
- Your server requires authentication or blocks Apple's verification request
- The file was modified or re-encoded (it must be served exactly as downloaded)
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:#
- Customer clicks Apple Pay button → your JS creates an
ApplePaySession - Safari fires the
onvalidatemerchantevent with avalidationURL(an Apple URL) - Your frontend sends this
validationURLto your backend - Your backend POSTs to the
validationURLusing the Merchant Identity Certificate (mTLS) with this JSON body:{ "merchantIdentifier": "merchant.com.yourcompany.store", "displayName": "Your Store Name", "initiative": "web", "initiativeContext": "www.yourstore.com" } - Apple returns a merchant session object (opaque JSON)
- Your backend returns this session to the frontend
- Your frontend calls
session.completeMerchantValidation(merchantSession) - 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#
- Sign in to App Store Connect
- Go to Users and Access → Sandbox → Testers
- Click + to create a new sandbox tester
- 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#
- On Mac (for Safari testing): Go to System Settings → Wallet & Apple Pay (or Internet Accounts on older macOS) and sign in with the sandbox tester Apple ID
- On iPhone/iPad: Go to Settings → Wallet & Apple Pay and add the sandbox account
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#
- Make sure your payment gateway is pointing to its sandbox/test environment (e.g.
pay-test.musqet.techfor Encoded, or your Cardstream test credentials) - Open your checkout page in Safari on the device signed into the sandbox account
- Click the Apple Pay button — the payment sheet will appear with the sandbox test cards
- Authorise the payment — the token will be sent to your sandbox gateway, which will process it without real charges
Important Notes#
- Real device required — Apple Pay on the Web cannot be tested in simulators or emulators. You need Safari on a real Mac or iOS device.
- Domain verification must work — there is no way to bypass domain verification or merchant validation in sandbox. Your
.well-known/apple-developer-merchantid-domain-associationfile must be accessible and your Merchant Identity Certificate must be configured. - Same certificates — the same Merchant ID, certificates, and domain verification are used in both sandbox and production. The only difference is which payment gateway environment you point your backend at.
- Sandbox test cards — Apple provides several test card numbers automatically. Transactions made with these cards through a sandbox gateway will not process real charges.
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:
- Load the library:
<script src="https://pay.google.com/gp/p/js/pay.js"></script> - Create a PaymentsClient with
environment: 'TEST' - Check readiness: Call
isReadyToPay()— only show the button if this returns true - Render the button: Use Google's pre-built button component
- Handle payment: Build a
PaymentDataRequestwith your merchant info, accepted card networks, and transaction amount - 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:
- No Google Business Profile or Merchant ID required — use any placeholder
- Google Pay provides its own test cards automatically — you won't be able to use real cards in TEST mode. The payment sheet clearly indicates you're in a test environment and no charges will be made
- Test the complete frontend flow: button display, payment sheet, and token extraction
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:
- Go to the Google Pay & Wallet Console
- Sign in with a Google account (or create one)
- Select Merchant as your business type
- Complete the required business information
- 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#
- In the Google Pay & Wallet Console, go to Google Pay API → Integrations
- Click Integrate with your website
- Click Add website and enter your top-level domain
- 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
- Upload screenshots of your checkout page showing the Google Pay button and payment flow
- Click Submit for review
Google's team will review your integration. This typically takes a few business days.
Step 5: Go Live#
Once approved:
- Update your
PaymentsClientto useenvironment: 'PRODUCTION' - Set
merchantInfo.merchantIdto your actual Merchant ID from the console - Set
merchantInfo.merchantNameto your business name - 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:
- Use Google's official button assets — do not create your own
- Only show the button after
isReadyToPay()confirms the user can pay - Match button dimensions to other payment buttons on your page
- Choose the appropriate button color variant for your page background (dark button on light background, light button on dark background)
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#
- v1.7 - Added comprehensive Apple Pay and Google Pay setup guides (gateway-agnostic); added Apple Pay support for Encoded gateway (native ApplePaySession + APM library); added Apple Pay and Google Pay examples for Encoded; added Apple Pay sandbox testing guide; fixed Encoded overview to reflect Apple Pay support
- v1.6 - Added Bitcoin DIY mode: native checkout modal that talks directly to the Musqet REST API with no iframe or hosted component; documented Checkout API endpoints (create, get, subscribe); added
bitcoin-create-checkout.phpandbitcoin-checkout-status.phpAPI endpoints; DIY is now the default Bitcoin example - v1.5 - Bitcoin examples moved to standalone
bitcoin-examples/directory; added iframe/redirect/modal checkout modes; added Get Checkout API verification; added configure pages and index.html redirects to all example directories; added credential validation to Encoded API endpoints; fixed Google Pay gateway name tocrst; added file trees for all example directories - v1.4 - Added Apple Pay support for Cardstream gateway (native ApplePaySession API with merchant validation)
- v1.3 - Added Google Pay support for Cardstream gateway (native Google Pay JS API)
- v1.2 - Separated Bitcoin into standalone section; removed Part 1/Part 2 naming
- v1.1 - Added Cardstream gateway examples and documentation
- v1.0 - Initial release with Encoded HPF, Google Pay, and Bitcoin support