Express Checkout
Express checkout lets you collect shipping and billing addresses directly from Apple Pay, Google Pay, or PayPal wallet UIs — no separate address form needed.
Available on PaymentRequest (Apple Pay / Google Pay) and PayPal components.
Before you begin
- You'll need a MONEI account and your API keys (test or live). Find them in your MONEI Dashboard.
- Use your test mode keys for integration testing.
- Apple Pay requires domain verification in both development and production.
- Google Pay requires at least one configured card processor.
- PayPal requires a connected PayPal business account.
Express checkout with shipping
With shipping, the wallet-authorized amount may differ from the initial amount when shipping costs change. This flow uses the accountId approach — no pre-created payment needed. The payment is created server-side after the customer confirms in the wallet UI.
1. Include monei.js (Client-side)
Add the script tag to the head of your HTML file.
<head>
<script src="https://js.monei.com/v3/monei.js"></script>
</head>
<body>
<!-- Use "payment_request" for Apple Pay / Google Pay, "paypal_container" for PayPal -->
<div id="payment_request"></div>
<div id="paypal_container"></div>
<script src="client.js"></script>
</body>
2. Initialize Component with express checkout (Client-side)
- Apple Pay / Google Pay
- PayPal
const paymentRequest = monei.PaymentRequest({
accountId: 'your_account_id',
sessionId: 'unique_session_id',
amount: 1099, // base amount in minor units (before shipping)
currency: 'EUR',
requestShipping: true,
requestBilling: true,
shippingOptions: [
{id: 'standard', label: 'Standard Shipping', amount: 500},
{id: 'express', label: 'Express Shipping', amount: 1200, description: '1-2 business days'}
],
onShippingAddressChange(address) {
// Called when the customer changes their shipping address.
// address contains: country, city, state, zip (no street-level data mid-flow).
// Return updated shipping options and/or amount based on the address.
if (address.country !== 'ES') {
return {
shippingOptions: [{id: 'intl', label: 'International', amount: 2000}],
amount: 3099 // product (1099) + intl shipping (2000)
};
}
return {shippingOptions: [{id: 'standard', label: 'Standard', amount: 500}], amount: 1599};
},
onShippingOptionChange(option) {
// Called when the customer selects a different shipping option.
// Return updated total amount.
return {amount: 1099 + option.amount};
},
async onSubmit(result) {
// Called when the customer approves the payment in the wallet UI.
// Create the payment server-side (see Step 3).
await fetch('/create-payment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
amount: result.finalAmount,
currency: 'EUR',
sessionId: 'unique_session_id',
paymentToken: result.token,
billingDetails: result.billingDetails,
shippingDetails: result.shippingDetails
})
});
},
onError(error) {
console.error(error);
}
});
paymentRequest.render('#payment_request');
const paypal = monei.PayPal({
accountId: 'your_account_id',
sessionId: 'unique_session_id',
amount: 1099,
currency: 'EUR',
requestShipping: true,
shippingOptions: [
{id: 'standard', label: 'Standard Shipping', amount: 500, selected: true},
{id: 'express', label: 'Express Shipping', amount: 1200}
],
onShippingAddressChange(address) {
return {
shippingOptions: [{id: 'standard', label: 'Standard', amount: 500, selected: true}],
amount: 1599
};
},
onShippingOptionChange(option) {
return {amount: 1099 + option.amount};
},
async onSubmit(result) {
// PayPal always returns payer info (name, email) in billingDetails.
// Create the payment server-side (see Step 3).
await fetch('/create-payment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
amount: result.finalAmount,
currency: 'EUR',
sessionId: 'unique_session_id',
paymentToken: result.token,
billingDetails: result.billingDetails,
shippingDetails: result.shippingDetails
})
});
},
onError(error) {
console.error(error);
}
});
paypal.render('#paypal_container');
PayPal always returns payer information (name, email) regardless of configuration — there is no separate requestBilling prop.
Key options:
- accountId
string: Your MONEI account ID. - sessionId
string: A unique session identifier. Must match thesessionIdpassed when creating the payment server-side. - amount
number: Initial amount in the smallest currency unit (e.g., 1099 = 10.99 EUR). - requestShipping
boolean: Enable shipping address collection in the wallet UI. - requestBilling
boolean: Enable billing address collection (PaymentRequest only — PayPal always returns billing info). - shippingOptions
ShippingOption[]: Available shipping options.
See PaymentRequest options and PayPal options for the full list.
3. Create Payment (Server-side)
When the customer confirms in the wallet UI, your onSubmit handler sends the result to your server. Create the payment passing the paymentToken — this confirms the payment automatically.
You must pass the same sessionId used to initialize the component on the frontend. This ensures the token-generating customer matches the paying customer.
- cURL
- Node.js
- PHP
- Python
curl --request POST 'https://api.monei.com/v1/payments' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 1599,
"currency": "EUR",
"orderId": "14379133960355",
"description": "Test Shop - #14379133960355",
"sessionId": "unique_session_id",
"paymentToken": "token_from_client",
"callbackUrl": "https://example.com/checkout/callback",
"customer": {
"email": "email@example.com"
},
"billingDetails": {},
"shippingDetails": {}
}'
import {Monei} from '@monei-js/node-sdk';
const monei = new Monei('YOUR_API_KEY');
// In your /create-payment route handler:
app.post('/create-payment', async (req, res) => {
const {amount, currency, sessionId, paymentToken, billingDetails, shippingDetails} = req.body;
const payment = await monei.payments.create({
amount,
currency,
orderId: '14379133960355',
description: 'Test Shop - #14379133960355',
sessionId,
paymentToken,
callbackUrl: 'https://example.com/checkout/callback',
customer: {email: billingDetails?.email},
billingDetails,
shippingDetails
});
res.json({status: payment.status});
});
<?php
require_once 'vendor/autoload.php';
use Monei\Model\CreatePaymentRequest;
use Monei\MoneiClient;
$monei = new MoneiClient('YOUR_API_KEY');
$body = json_decode(file_get_contents('php://input'), true);
$payment = $monei->payments->create(
new CreatePaymentRequest([
'amount' => $body['amount'],
'currency' => $body['currency'],
'order_id' => '14379133960355',
'description' => 'Test Shop - #14379133960355',
'session_id' => $body['sessionId'],
'payment_token' => $body['paymentToken'],
'callback_url' => 'https://example.com/checkout/callback',
'billing_details' => $body['billingDetails'],
'shipping_details' => $body['shippingDetails']
])
);
echo json_encode(['status' => $payment->getStatus()]);
?>
import Monei
from Monei import CreatePaymentRequest
monei = Monei.MoneiClient(api_key="YOUR_API_KEY")
# In your /create-payment route handler:
payment = monei.payments.create(
CreatePaymentRequest(
amount=body["amount"],
currency=body["currency"],
order_id="14379133960355",
description="Test Shop - #14379133960355",
session_id=body["sessionId"],
payment_token=body["paymentToken"],
callback_url="https://example.com/checkout/callback",
billing_details=body["billingDetails"],
shipping_details=body["shippingDetails"]
)
)
Check all available request parameters.
4. Process Webhook Notification (Server-side)
MONEI sends the final payment status via an asynchronous HTTP POST request to the callbackUrl you provided in Step 3. The request body contains the full Payment object in JSON format.
This webhook is the only reliable way to confirm the definitive payment outcome.
- Verify the
MONEI-Signatureheader to confirm the webhook genuinely came from MONEI. See the Verify Signatures guide for implementation details. - Return a
200 OKHTTP status code immediately to acknowledge receipt.
If MONEI doesn't receive a 200 OK, it will retry sending the webhook.
Billing-only checkout
If you only need the billing address (no shipping), the flow is simpler — use paymentId instead of accountId and confirm client-side with confirmPayment. Follow the same steps as Build a custom checkout, but add requestBilling: true to collect the billing address from the wallet:
const paymentRequest = monei.PaymentRequest({
paymentId: '{{payment_id}}',
requestBilling: true,
async onSubmit(result) {
await monei.confirmPayment({
paymentId: '{{payment_id}}',
paymentToken: result.token,
billingDetails: result.billingDetails
});
},
onError(error) {
console.error(error);
}
});
paymentRequest.render('#payment_request');
Type reference
See the full type definitions in the Reference:
SubmitResult— returned by allonSubmitcallbacksShippingOption— shipping option configurationShippingAddressChangeResult— return type foronShippingAddressChangeShippingOptionChangeResult— return type foronShippingOptionChangeBillingDetails— billing/shipping detailsAddress— postal address
Before you go live
- Make sure you are using live (production) mode Account ID and API Key.
- Make sure you have the relevant payment methods configured and enabled in live mode.
- For Apple Pay, verify your production domain.