# Express Checkout integration guide

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

Follow the steps below to add [Express Checkout](/platforms/embedded-payments/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:

```html
<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](/platforms/embedded-payments/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](/platforms/embedded-payments/instances/)).

The script at this URL is the standard shuttle.js plus the merchant's Express Checkout and [BNPL Messaging](/platforms/embedded-payments/bnpl-messaging/) set-up, so it also runs [Checkout](/platforms/embedded-payments/checkout/) and the other components. A page that uses several of them needs only this one script tag.

:::note[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](/platforms/embedded-payments/shuttlejs/) 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](/platforms/reference/operations/listgateways/) 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

```bash
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, 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.

```html
<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`:

```javascript
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](/platforms/embedded-payments/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](/platforms/embedded-payments/express-checkout-options/#callbacks).

## 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.

```javascript
// 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](/platforms/reference/operations/createpayment/) 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](/platforms/reference/operations/createpaymentmethod/):

```javascript
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](/platforms/reference/webhooks/) for the full list.