Skip to main content

Payment flows

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 (API docs) 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.

mutation paymentInstructions($input: PaymentInstructionsInput) {
paymentInstructions(input: $input) {
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.

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 3DS 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 (it grows depending on how many items the selection contains for example). If you need to use something other 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 Storefront 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)

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);

return { ...postData, ...getData };
}

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

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
}
}
}