Checkout

Last updated

Introduction#

The Checkout field on the Selection compliments the fields exposed along side it to build a fully functional Checkout. It’s worth noting that querying the Checkout field is the only way to trigger shipping and tax calculations as you typically don’t have access to the details needed until you get to the Checkout, such as a full ship-to address.

It is advisory to always query Checkout field if the customer is on the checkout page or the mutation was instantiated from the checkout page.

As the checkout, and especially payments, is the most complex part of any ecommerce site build, make sure you understand and follow these instructions. Even if you don’t encounter a certain PaymentAction during development once you hit production you might encounter new flows due to configuration changes within a PSP etc.

Elements of the checkout#

There are a few key elements to any Checkout; showing the contents and totals of the cart, modifying the cart contents (quantities, adding upsell products shown on the Checkout) handling of code vouchers, collecting the shoppers address, how the goods shall be delivered, choosing the paymentMethod and finally collecting the payment. Of course all of these different elements interact with each other and need to be represented accurately as data change. Thankfully the DTC API always returns the last valid state of the Selection and Checkout, so even if you get any userErrors you can rely on what you got back.

As always it’s good to have a solid understanding of each field’s contents, purpose and dependencies so let’s break them down.

AddressFields#

Address form fields the DTC API accepts as shopper input to the setAddressand paymentInstructions mutations. They are divided into shippingAddress, separateBillingAddress, termsAndConditions and newsletter.

The AddressField interface exposes three properties; key, visible and required. It’s recommended that you used these to correctly collect all needed information about the shopper before continuing to the payment.

The list of required and visible fields is dynamic depending on your country and your selected Pa`mentMethod. As Sweden doesn’t have states the state field won’t be set as visible nor required, but for the U.S. it will and some PaymentMethods allows you to collect the address either on their Hosted Payment Page or within their iframe you display on the checkout (“address after payment”), in that case only the shippingCountry.country (and state if applicable) will be required and visible. The Centra admin has the option to configure the required fields further in the DTC API plugin. Please note that for “address after payment” type of payment methods, these can be overruled.

The shippingAddress and separateBillingAddress are represented as lists, allowing you to loop over them to generate the form and bind each input to their corresponding field in the inputs for [setAddress|paymentInstructions|.[shipping|separateBilling]Address. The current value of each field can be found in the [shipping|separateBilling]Address properties of the Checkout object. As the schema suggests there's a default sort order of the fields which you can override depending on your design needs.

PaymentMethods#

PaymentMethods will list the available payment methods for the current selection

The availability of a PaymentMethod can be configured by the Centra admin using the following filters shippingAddress.country, market, pricelist, and language.

There is additional filtering done by the DTC API after the payment methods have been filtered based on their configuration;

  • If the contents of the cart doesn’t meet

the minimum order value required by the PSP it will be filtered out.

  • If the cart contains any line with a subscription plan attached to it and the PSP doesn’t support recurring payments, it will be filtered out.

PaymentMethods are always filtered by the DTC API based on the data it has, and you should always show all returned.

If there hasn’t been any active choice of PaymentMethod the first one will always be the active one, you can control the sort order of the PaymentMethods based on its kind. Per default they are sorted by the internal plugin name.

Any logos or similar should be based on the PaymentMethod.kind field and not bound to an id or name as these are subject to change.

The DTC API has support for following PaymentMethods:

Payment pluginPaymentMethodKind
Adyen Drop-inADYEN_DROPIN
Test Payment (Dummy)DUMMY
External PaymentEXTERNAL_PAYMENT
Klarna Checkout V3KLARNA_CHECKOUT_V3
Klarna PaymentsKLARNA_PAYMENTS
PayPal CommercePAYPAL_COMMERCE
Qliro OneQLIRO_ONE
Stripe Checkout (Beta)STRIPE_CHECKOUT
Stripe Payment Intents (Beta)STRIPE_PAYMENT_INTENTS

ShippingMethods#

ShippingMethods will list The available shipping methods configured within Centra for the current selection

The availability of a ShippingMethod is configured based on the Selection's Currency, market, country and state(if applicable). Additionally in order for a ShippingMethod to be available at least one range criteria must be met, e.g. if you have a ShippingMethod connected to the SEK currency where one has a price range for Sweden starting from 500 SEK, it won’t be available until the cart has reached that value.

The price of the ShippingMethod depends on its ranges and are configured by the Centra admin, but can also be subject to vouchers that set the shipping cost to 0 for given ShippingMethod.

Totals#

Totals will list the summary of all order totals and taxes, represented as a list.

You will always get all totals back.

ITEMS_SUBTOTAL

  • The subtotal of lines, including line level discounts. DISCOUNT

  • The total order level discount, e.g. from a voucher. CREDIT

  • The total amount applied from credit vouchers. SHIPPING

  • The shipping cost. HANDLING

  • Eventual handling cost. GRAND_TOTAL

  • The summarized total that the customer will pay, tax taken into account (added, deducted, included).

The following fields depend on the Tax rule setup for the shippingAddress

Tax rule (include tax)Type
In prices displayed before the checkout and in the checkoutTAX_INCLUDED
In prices displayed before the checkout but not in the checkoutTAX_DEDUCTED
On top of prices in the checkoutTAX_ADDED

All tax fields implement SelectionTotalTaxRow, gaining you access to the percentage.

Widgets#

A list of widgets available for the selection, exposes integrations prebuilt by Centra that you can use on the checkout page.

Implementing the checkout#

Since mutations on the selection pre checkout and on the checkout share some concepts, operations such as updating line quantities and adding items have been left out. The only difference is that on the checkout page you would query the Checkout field on the SelectionPayload.selection returned.

Dealing with errors#

The DTC API has two concepts of errors, userErrors which are logical errors and errors which are either validation errors on the GraphQL level or an internal system error. If you would encounter the latter please reach out to partnersupport@centra.com.

The userError type is an interface giving you a generic message of what went wrong and the path of where it went wrong, you can further inspect the error by using fragments of the different implementations and craft your own error messages shown to the shopper with as many details as you wish. Even if you get userErrors back you can still update your state with the other data returned, no need to refetch it.

Getting the checkout specific data#

Below you can find an example of the data you would query whilst on the checkout page, whether it’s reading the contents when the shopper first lands on the checkout page or performs a mutation whilst there.

For simplicity we will use the SelectionCheckoutFragment in all examples going forward.

NOTE: to make sure your checkout is always up-to-date, it is advisory to reuse the same fragment for all checkout operations, and pass it to your components.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 fragment CheckoutFragment on CheckoutSelection { shippingMethods { id name comment selected price { formattedValue } } paymentMethods { id name active addressAfterPayment kind initiateOnlySupported recurringSupported } widgets { kind ... on IngridWidget { snippet sessionId ingridAttributes deliveryOptionsAvailable reload } } addressFields { shippingAddress { key visible required ... on CountryAddressField { choices { name code } } ... on StateAddressField { choices { name code } } } separateBillingAddress { key visible required } newsletter { key visible required } termsAndConditions { key visible required } } hasSeparateBillingAddress shippingAddress { firstName lastName address1 address2 city zipCode stateOrProvince phoneNumber email country { name code } state { name code } } separateBillingAddress { firstName lastName address1 address2 city zipCode stateOrProvince phoneNumber email country { name code } state { name code } } totals { price { formattedValue } ... on SelectionTotalTaxRow { taxPercent } type } } fragment SelectionCheckoutFragment on Selection { id lines { ...LineFragment } checkout { ...CheckoutFragment } language { name code } discounts { name appliedOn method type value { value formattedValue } ... on CodeVoucher { code } ... on UrlVoucher { url } } attributes { type { name } elements { key kind ... on AttributeChoiceElement { value { name value } values { name value } } ... on AttributeFileElement { url } ... on AttributeImageElement { url width height mimeType } ... on AttributeStringElement { value translations { language { name code } value } } } ... on MappedAttribute { id } } } fragment LineFragment on Line { ...ProductLineFragment ... on BundleLine { bundle { id type priceType sections { id quantity lines { ...ProductLineFragment } } } } } fragment ProductLineFragment on ProductLine { id name productVariantName size productNumber comment addedFromCategory { id name } productExternalUrl brand { name } item { id name sku GTIN preorder stock { available } } appliedPromotions { type percent value { value formattedValue } } hasDiscount unitPrice { value formattedValue } unitOriginalPrice { value formattedValue } quantity subscriptionId displayItem { id } }

Rendering the checkout#

We start off with a query to render the full checkout page:

1 2 3 4 5 query GetCheckout { selection { ...SelectionCheckoutFragment } }

The DTC API offers two ways to set the customer’s address, setAddress and paymentInstructions. The reason for this is that you might want to gather information as they fill out the address form, for example if you want to trigger cartAbandonment, before they start the payment.

Changing the shippingAddress.[country|stateOrProvince] in by providing it in an AddressInput triggers the same logic as if you would change it via the setCountryState mutation, so market and pricelist are be affected by this mutation, and might cause lines to be removed from the Selection. In such case you will receive an UserError implementation, UnavailableItem, detailing which line and the item and displayItem info regarding it.

Collecting the customer info before going to payment

The setAddress mutation allows you to collect incremental address data about the shopper as they fill out the address form.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 mutation setAddress($shippingAddress: AddressInput!, $separateBillingAddress: AddressInput) { setAddress( sendCartAbandonmentEmail: true shippingAddress: $shippingAddress separateBillingAddress: #separateBillingAddress ) { selection { ...SelectionCheckoutFragment } userErrors { message path ... on UnavailableItem { item { id name } originalQuantity availableQuantity unavailableQuantity displayItem { name productVariant { name } } } } } }

If you trigger the setAddress mutation on each action of your address field it’s advisable to gather the events and send a bigger call instead of many small ones since the browser’s prefill of addresses will trigger many calls.

Vouchers#

You can add as many vouchers as you like to the selection by using the addVoucher mutation. However, depending on their configuration they might not all apply to the selection.

1 2 3 4 5 6 7 8 9 10 11 mutation AddVoucher($code: String!) { addVoucher(code: $code) { selection { ...SelectionCheckoutFragment } userErrors { message path } } }

NOTE: Since Centra supports URLVouchers and auto vouchers as well, there are vouchers which might have been applied before reaching the checkout, so they are visible outside of the checkout field on the Selection

Removing a voucher#

The shopper might not want to utilize the voucher for this purchase after they see how it’s been applied so they have the option to remove it as well.

1 2 3 4 5 6 7 8 9 10 11 mutation RemoveVoucher($code: String!) { removeVoucher(code: $code) { selection { ...SelectionCheckoutFragment } userErrors { message path } } }

Shipping#

Changing shipping method

1 2 3 4 5 6 7 8 9 10 11 mutation UpdateShippingMethod($id: Int!) { setShippingMethod(id: $id) { selection { ...SelectionCheckoutFragment } userErrors { message path } } }

Payment flow#

Once the customer has selected their shipping method, payment method, filled out their address etc then it’s time to start the payment flow.

Requesting the payment instructions#

The first part of the payment flow is to request the paymentInstructions for the given paymentMethod, which will instruct you on which action to need to take further. For some paymentMethods you will use paymentInstructions multiple times to handle callbacks from actions the shopper takes. It’s therefore required that you implement all PaymentActions to have a fully functioning checkout.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 mutation paymentInstructions( input: { affiliate: Int integrationSpecificFields: Map checkoutPageOrigin: String customerClubSpecificFields: CustomerClubSpecificFields consents: [ConsentInput!] comment: String internal: Boolean! = false ipAddress: String """ The languageCode for the language selected by the shopper, if empty the language of the session will be used. """ languageCode: String! = "" paymentInitiateOnly: Boolean! = false """ The paymentMethod, if empty the paymentMethod of the selection will be used. """ paymentMethod: Int! = 0 paymentMethodSpecificFields: Map paymentFailedPage: String! """ The page hosted by you which the PSP will send the customer to after the payment information has been collected. From this page you will call the `paymentResult` mutation to validate the payment. This page MUST be hosted server side since PSPs might redirect you using the HTTP POST method. """ paymentReturnPage: String! shippingAddress: AddressInput! separateBillingAddress: AddressInput termsAndConditions: Boolean! paymentInitiateOnly: Boolean! = false """ The shippingMethod, if empty the shippingMethod of the selection will be used. """ shippingMethod: Int! = 0 #use default based on country and state if null / from selection data } ) { paymentInstructions (input: PaymentInstructionsInput!) { action { action ... on FormPaymentAction { html formFields formType } ... on JavascriptPaymentAction { formFields formType script } ... on RedirectPaymentAction { url } ... on SuccessPaymentAction { order { number #... } } } userErrors { path message } } }

Handling payment actions#

As you can see from the paymentInstructions mutation four different actions can be returned.

Some PaymentActions will dispatch a CustomEvent, centra_checkout_payment_callback; which the checkout page must handle.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 document.addEventListener('centra_checkout_payment_callback', function(origdata) { var postData = origdata.detail; var responseEventRequired = postData.responseEventRequired; var addressIncluded = postData.addressIncluded; const response = await PaymentInstructionsMutation({ address: addressIncluded ? postData.shippingAddress : shippingAddressFromCheckout(), separateBillingAddress: addressIncluded ? postData.billingAddress : billingAddressFromCheckout(), paymentMethodSpecificFields: postData.paymentMethodSpecificFields }); if (responseEventRequired) { if(data.action === REDIRECT) { location.href = data.url; return; } if (data.errors) { // Payment failed for some reason, show error ShowPaymentFailedError(data.errors); return; } // action is javascript, send back the formFields sendCentraEvent("centra_checkout_payment_response", response.formFields); } });

NOTE: Each paymentMethod can implement multiple paymentActions, depending on the input provided to paymentInstructions.

FormPaymentAction

If you receive the FormPaymentAction back, you are supposed to insert the contents into the DOM. For frameworks such as React it’s important to insert it unescaped and also evaluate the response. This response is typically returned for payment widgets such as Klarna Checkout and Adyen Drop-in, but could also also just be a form which will be submitted to a Hosted Payment Page.

JavascriptPaymentAction

If the JavascriptPaymentAction is returned, it means you are required to execute the code that’s returned. An example of this can be displaying the inline 3DS2 inline for Adyen Drop-In.

RedirectPaymentAction

If you receive the RedirectPaymentAction you should redirect the customer to the page in the URL.

SuccessPaymentAction

The SuccessPaymentAction means that the order has been placed, such cases can happen if you for instance have a 100% off voucher on the order, and then payment isn’t needed to place the order. This does not apply to addressAfterPayment payment methods which handle 0 amount orders.

If you receive the SuccessPaymentAction you should redirect the customer to the “Thank you” page which will call the order mutation. Since the order has already been placed in this case, there’s no need to do a paymentResult call.

Verifying the payment with paymentResult#

After the PSP has collected all necessary payment details from the customer, the customer will be redirected to the paymentReturnPage provided in the paymentInstructions mutation. The responsibility of this mutation is to verify the payment, and if successful the selection will be turned into an order.

Parameters sent to the paymentReturnPage

The paymentReturnPage should always collect all URL-parameters from both the query string in the URL and the POST-data and send it to Centra. This is the way to validate if the payment went through successfully or not. Some payment methods used through Adyen Drop-In will use POST-data instead of sending back the parameters as query string parameters.

Also, if you're running serverless, you can not use an endpoint to convert the POST-data into query-parameters directly, since the payload from Adyen can be really large (since it grows depending on how many items the selection contains for example). If you need to use something else than POST-data, you can have an endpoint receiving the POST-data and converts it to fragment-data, like this: https://example.com/thank-you#centraPaymentMethod=x&payload=xyz, that way you will not hit any issues with too long URLs. You then need to parse the fragment part just like query parameters to send the data to the PaymentResult mutation.

Another solution would be to have an endpoint that injects the data into the DOM as JSON (using the example code below) to then send the data to Centra.

To make sure you support both POST/GET requests and outputs the data into the DOM properly (for you javascript to pick up the parameters and send them to the DTC API’s paymentResult mutation, here's example code for Node.js to collect POST/GET-parameters into a variable in the DOM. The code below also supports associative arrays in POST (like details[paymentAction]=xxx)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function toJSON(formData) { split = /[\[\]]+/; fields = Array.from(formData.keys()); values = Array.from(formData.values()); const hierarchyFields = fields.map(field => field.split(split).filter(floor => floor !== "")) const data = values.reduce( (data,value,index) => { let swap = data let h = hierarchyFields[index] h.forEach((floor,index,fieldsHierarchy) => { if(!fieldsHierarchy[index + 1]){ swap[floor.replace("[]","")] = value return } if(!swap[floor]){ swap[floor] = {}; if(!isNaN(parseInt(fieldsHierarchy[index + 1]))){ swap[floor] = []; } } swap = swap[floor]; }) return data },{}) return data; } async function convertPostAndGetToJSON(request) { let postData = {} try { postData = toJSON(await request.formData()); } catch(e) { } const getData = toJSON(await new URL(request.url).searchParams); Object.assign(postData, getData) return postData; }

Once you’ve collected the parameters you send them to the DTC API in the paymentMethodFields, note that it accepts it as a Map: scalar so you need to send it as an object.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 mutation PaymentResult($paymentMethodFields: Map!) { paymentResult(paymentMethodFields: $paymentMethodFields) { type selection { ...SelectionFragment } ... on PaymentResultSuccessPayload { order { ...OrderFragment } } userErrors { path message } } } fragment OrderFragment on Order { id number status shippingMethod { id name price { formattedValue } } paymentMethod { id name kind } shippingAddress { firstName lastName address1 address2 city zipCode stateOrProvince cellPhoneNumber faxNumber email companyName attention vatNumber country { name code } state { name code } } billingAddress { firstName lastName address1 address2 city zipCode stateOrProvince cellPhoneNumber faxNumber email companyName attention vatNumber country { name code } state { name code } } paymentHtml affiliateHtml totals { type price { formattedValue } ... on SelectionTotalTaxRow { taxPercent } } }

Showing the last placed order#

The last order placed for the session is available under the order query, typically used on the “Thank you” page. Using this query allows the customer to see their last order, or if they go back to the "Thank you" page or reloads it.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 query GetLastOrder { order { ...OrderFragment } } fragment OrderFragment on Order { id number status shippingMethod { id name price { formattedValue } } paymentMethod { id name kind } shippingAddress { firstName lastName address1 address2 city zipCode stateOrProvince cellPhoneNumber faxNumber email companyName attention vatNumber country { name code } state { name code } } billingAddress { firstName lastName address1 address2 city zipCode stateOrProvince cellPhoneNumber faxNumber email companyName attention vatNumber country { name code } state { name code } } paymentHtml affiliateHtml totals { type price { formattedValue } ... on SelectionTotalTaxRow { taxPercent } } }