Checkout
Last updatedIntroduction#
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 setAddress
and 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 plugin | PaymentMethodKind |
---|---|
Adyen Drop-in | ADYEN_DROPIN |
Test Payment (Dummy) | DUMMY |
External Payment | EXTERNAL_PAYMENT |
Klarna Checkout V3 | KLARNA_CHECKOUT_V3 |
Klarna Payments | KLARNA_PAYMENTS |
PayPal Commerce | PAYPAL_COMMERCE |
Qliro One | QLIRO_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 checkout | TAX_INCLUDED |
In prices displayed before the checkout but not in the checkout | TAX_DEDUCTED |
On top of prices in the checkout | TAX_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
}
}
}