Express Checkout integration guide

Follow the steps below to add Express Checkout to a page:

  1. (clientside) Load shuttle.js with your shared key and the merchant's instance key in the URL
  2. (clientside) Add an element for the buttons, and pass your options to shuttle.js
  3. (clientside) Handle the on_start, on_success and on_error callbacks
  4. (serverside) Create the payment or payment method from gateway_data
  5. (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 URL

The content at this URL is different for each merchant and changes when they change payment providers, so an integrity checksum cannot match it: leave integrity out. 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 returned
  • AUTHENTICATE_CAPTURE_SHIPPING: the customer can choose or enter a shipping address with this provider, and it is returned
  • AUTHENTICATE_PAYMENT_TOKEN: the customer can choose or enter a payment method with this provider, and gateway_data is 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 receive account, shipping (if you set capture_shipping) and gateway_data (if you set payment_token). Send them to your server.
  • on_error: the customer cancelled (cancel is true), or something went wrong (error holds 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: PAYMENT takes the money now; AUTH authorises it for you to capture later.
  • nonce: derive it from the order. Shuttle treats a second request with the same nonce within 24 hours as a duplicate, so a retry does not become a second payment.
  • save_card: add true to keep the payment method for future payments as well.
  • crm_key: your own identifier for the customer, if you have one. account.gateway_key is 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.FAILED and PAYMENT.PENDING for payments
  • PAYMENT_METHOD.ACTIVE, PAYMENT_METHOD.FAILED and PAYMENT_METHOD.PENDING for saved payment methods

See Webhooks for the full list.


Did this page help you?