Express Checkout integration guide
Follow the steps below to add Express Checkout to a page:
- (clientside) Load
shuttle.jswith your shared key and the merchant's instance key in the URL - (clientside) Add an element for the buttons, and pass your options to shuttle.js
- (clientside) Handle the
on_start,on_successandon_errorcallbacks - (serverside) Create the payment or payment method from
gateway_data - (serverside) Handle webhooks
Loading shuttle.js
Express Checkout needs the copy of shuttle.js that Shuttle builds for the merchant. Put your shared key and the merchant's instance key in the URL:
<script src="https://app.shuttleglobal.com/${shared_key}/${instance_key}/shuttle-1.4.7.js" type="text/javascript"></script>shared_key: the shared key of your application environment, shown next to its secret key on your application's Developer tab (see Applications). Use the sandbox shared key for sandbox instances and the live one for live instances. Never put a secret key in a page.instance_key: the merchant's instance key (see Multi-tenancy / instances).
The script at this URL is the standard shuttle.js plus the merchant's Express Checkout and BNPL Messaging set-up, so it also runs Checkout and the other components. A page that uses several of them needs only this one script tag.
No SRI checksum on this URLThe content at this URL is different for each merchant and changes when they change payment providers, so an
integritychecksum cannot match it: leaveintegrityout. Still pin the version (shuttle-1.4.7.js). See shuttle.js for the available versions.
Shuttle includes each payment provider the merchant has connected and set up to take payments, where that provider supports Express Checkout. The script is cached, so a provider the merchant has only just connected may not show buttons straight away.
Checking availability (optional)
Shuttle.authenticate returns a Promise that resolves to { is_available: true } when at least one provider rendered buttons, and { is_available: false } when none did. Use it to hide an "Express checkout" heading or divider that would otherwise sit above nothing. This is the definitive check.
If you would rather know before the page is built, list the merchant's gateways with the List gateways API and look at each gateway's features:
AUTHENTICATE: the customer can sign in with this provider, and their details are returnedAUTHENTICATE_CAPTURE_SHIPPING: the customer can choose or enter a shipping address with this provider, and it is returnedAUTHENTICATE_PAYMENT_TOKEN: the customer can choose or enter a payment method with this provider, andgateway_datais returned
curl -u "$SHUTTLE_SECRET_KEY:" \
"https://app.shuttleglobal.com/c/api/instances/M12345/gateways?criteria=status%3DACTIVE%3BLINKED"Do this when the merchant's configuration changes and store the result, rather than for every basket. A gateway can report a feature and still render nothing, for example Stripe until Enable Express Checkout (eg Apple Pay / Google Pay) is turned on, so treat the list as a guide and is_available as the answer.
Adding the buttons
Add an empty element where the buttons should appear, then give shuttle.js your options in one of two ways.
Global variable (fastest). Set window.shuttle_authenticate_options before the shuttle.js script tag. shuttle.js renders the buttons as soon as the page has loaded, and starts loading the providers' libraries straight away. Put both script tags at the bottom of the page so they don't hold up the rest of it.
<div id="shuttle-express-checkout"></div>
<script>
window.shuttle_authenticate_options = {
element: "shuttle-express-checkout",
on_start: function (event) {
// The customer has started; pause anything that would interrupt them
},
on_success: function (result) {
// Nothing has been charged yet: send the result to your server
fetch("/express-checkout/complete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(result)
});
},
on_error: function (error) {
if (!error.cancel) {
// Show error.error to the customer
}
},
options: {
action: "AUTHORISE",
payment_token: true,
capture_shipping: true,
currency: "USD",
amount: "54.97"
}
};
</script>
<script src="https://app.shuttleglobal.com/${shared_key}/${instance_key}/shuttle-1.4.7.js" type="text/javascript"></script>Function. Call Shuttle.authenticate(options) when you are ready to render, for example in a single page app once the element exists. The providers' libraries are not loaded until you call it. Use this form if you want is_available:
Shuttle.authenticate({
element: "shuttle-express-checkout",
on_start: onStart,
on_success: onSuccess,
on_error: onError,
options: {
action: "TOKENISE",
payment_token: true,
capture_shipping: true,
currency: "USD"
}
}).then(function (result) {
if (!result.is_available) {
document.getElementById("express-checkout-section").hidden = true;
}
});If the basket changes, call Shuttle.authenticate again with the new options. It empties the element and renders the buttons again.
Every option is listed in Express Checkout options.
Handling the callbacks
on_start: the customer has started using a provider's buttons. PayPal passes{ modal: true }and covers your page with its own overlay, so this is the moment to pause timers or block other input. Not every provider calls it.on_success: the customer has finished with the provider. Nothing has been charged and nothing has been created in Shuttle. You receiveaccount,shipping(if you setcapture_shipping) andgateway_data(if you setpayment_token). Send them to your server.on_error: the customer cancelled (cancelistrue), or something went wrong (errorholds a message). Show errors to the customer; a cancel needs no message.
The full payloads are in Express Checkout options.
Completing the payment
Your server turns gateway_data into a payment or a payment method with your secret key. Pass gateway_data unchanged: its content differs by provider, and Shuttle reads the gateway to use from it. Use it once.
Take the amount from your own basket, not from the browser. With AUTHORISE, it must be the amount and currency the customer approved. With TOKENISE, it is the final total.
// Node 18+ with Express
app.post('/express-checkout/complete', async (req, res) => {
const { account = {}, shipping, gateway_data } = req.body
const order = await getOrder(req.session.orderId) // your own basket
const auth = Buffer.from(`${process.env.SHUTTLE_SECRET_KEY}:`).toString('base64')
const response = await fetch(`https://app.shuttleglobal.com/c/api/instances/${order.instanceKey}/payments`, {
method: 'POST',
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
payment: {
nonce: `order-${order.id}`,
action: 'PAYMENT',
amount: order.total,
currency: order.currency,
account: {
crm_key: order.customerId,
first_name: account.first_name,
last_name: account.last_name,
email: account.email,
phone: account.phone
},
shipping,
gateway_data
}
})
})
const { payment } = await response.json()
res.json({ status: payment.status })
})action:PAYMENTtakes the money now;AUTHauthorises it for you to capture later.nonce: derive it from the order. Shuttle treats a second request with the samenoncewithin 24 hours as a duplicate, so a retry does not become a second payment.save_card: addtrueto keep the payment method for future payments as well.crm_key: your own identifier for the customer, if you have one.account.gateway_keyis the provider's identifier for them; keep it if you want to recognise a returning customer.
Check payment.status in the response: SUCCESS, PENDING (wait for the webhook) or DECLINED. See Create a payment for every field.
To save a payment method without taking a payment, send the same account and gateway_data as a payment_method to the Payment Methods API:
const response = await fetch(`https://app.shuttleglobal.com/c/api/instances/${instanceKey}/payment_methods`, {
method: 'POST',
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method: {
account: {
crm_key: customerId,
first_name: account.first_name,
last_name: account.last_name,
email: account.email,
phone: account.phone
},
gateway_data
}
})
})Handling webhooks
A payment or payment method created from Express Checkout raises the same webhooks as any other. To cover pending payments, and requests that timed out before your server saw the response, handle:
PAYMENT.SUCCESS,PAYMENT.FAILEDandPAYMENT.PENDINGfor paymentsPAYMENT_METHOD.ACTIVE,PAYMENT_METHOD.FAILEDandPAYMENT_METHOD.PENDINGfor saved payment methods
See Webhooks for the full list.
Updated about 6 hours ago