Storefront Integration
The Adyen App can be used to process payments using the Adyen API. By using Saleor's standard GraphQL payment APIs, one can interact with Adyen to authorize, capture, refund, and cancel payments.
Getting payment gateways
The first step is to fetch the Checkout object including availablePaymentGateways
field. The availablePaymentGateways
field contains a list of payment gateways available for given checkout. The Adyen App should be one of the payment gateways available in the list. Its id
is app.saleor.adyen
- defined in app's manifest.
query {
checkout(id: "Q2hlY2tvdXQ6YWY3MDJkMGQtMzM0NC00NjMxLTlkNmEtMDk4Yzk1ODhlNmMy") {
availablePaymentGateways {
id
name
}
}
}
The response:
{
"data": {
"checkout": {
"availablePaymentGateways": [
{
"id": "app.saleor.adyen",
"name": "Adyen"
}
]
}
}
}
availablePaymentGateways
may contain other Payment Apps as well as legacy Plugins configured in the Dashboard. You should ignore the ones that you don't want to use for a specific checkout.
Obtaining Adyen payment methods
Next, you need to fetch configured payment methods from Adyen. To do that, use the paymentGatewayInitialize
mutation. The mutation returns an PaymentGatewayInitialize
object with data
field containing a list of payment methods. The data
field is an object with the following fields:
{
paymentMethodsResponse: PaymentMethodsResponse;
clientKey: string;
environment: "LIVE" | "TEST";
errors?: SyncWebhookAppErrors;
}
Where PaymentMethodsResponse
is the result of calling Adyen's /paymentMethods
endpoint and is described in the Adyen documentation. SyncWebhookAppErrors
is described below.
If errors
field doesn't exist or is an empty array, paymentMethodsResponse
, clientKey
and environment
should be used to initialize Adyen Drop-in.
mutation {
paymentGatewayInitialize(
id: "Q2hlY2tvdXQ6YWY3MDJkMGQtMzM0NC00NjMxLTlkNmEtMDk4Yzk1ODhlNmMy"
amount: 54.24
paymentGateways: [{ id: "app.saleor.adyen" }]
) {
gatewayConfigs {
id
data
errors {
field
message
code
}
}
errors {
field
message
code
}
}
}
The response:
{
"data": {
"paymentGatewayInitialize": {
"gatewayConfigs": [
{
"id": "app.saleor.adyen",
"data": {
"paymentMethodsResponse": {
"paymentMethods": [
{
"brands": ["visa", "mc"],
"name": "Credit Card",
"type": "scheme"
}
]
},
"clientKey": "test_AHSJKADHK12731KDSALD11DSADASA003",
"environment": "TEST"
},
"errors": []
}
],
"errors": []
}
}
}
For instructions on how to add, remove or constraint payment methods from Adyen, please consult the Adyen payment methods documentation.
Paying with Adyen
After a user has interacted with the Adyen Drop-in and entered payment details, Drop-in event data along with other information should be passed to the transactionInitialize
mutation as the paymentGateway.data
field. The mutation returns the TransactionInitialize
object with a data
field containing the following fields:
{
paymentResponse: PaymentResponse;
errors?: SyncWebhookAppErrors;
}
Where PaymentResponse
is the result of calling Adyen's /payments
endpoint and is described in the Adyen documentation. SyncWebhookAppErrors
is described below.
If the errors
field doesn't exist or is an empty array, pass the paymentResponse
to Adyen Drop-in. The Drop-in will handle the response and display the result to the user or require additional actions to proceed.
mutation AdyenTransactionInitialize($data: JSON!) {
transactionInitialize(
id: "Q2hlY2tvdXQ6YWY3MDJkMGQtMzM0NC00NjMxLTlkNmEtMDk4Yzk1ODhlNmMy"
action: AUTHORIZATION
amount: 54.24
paymentGateway: { id: "app.saleor.adyen", data: $data }
) {
transactionEvent {
pspReference
amount {
amount
currency
}
type
}
data
errors {
field
message
code
}
}
}
Where $data
is the object provided by Adyen Drop-in in the onSubmit
(Web, React Native), didSubmit
(iOS) or makePaymentsCall
(Android) callback. Specifically, the following fields may be passed inside the $data
:
paymentMethod
(required)browserInfo
(required in browsers)returnUrl
(required for some payment methods)origin
(optional)channel
(optional,"Web"
by default)order
(optional)
Moreover, Saleor Adyen App automatically provides the following fields to Adyen:
reference
shopperReference
additionalData.manualCapture (when action is authorization)
merchantAccount
countryCode
shopperLocale
amount
authenticationData.threeDSRequestData.nativeThreeDS
("preferred"
)metadata
({ transactionId, channelId, checkoutId, orderId }
)lineItems
shopperEmail
shopperName
telephoneNumber
deliveryAddress
billingAddress
company.name
Response:
{
"data": {
"transactionInitialize": {
"transactionEvent": {
"pspReference": "XXXX9XXXXXXXXX99",
"amount": {
"amount": 54.24,
"currency": "EUR"
},
"type": "AUTHORIZATION_SUCCESS"
},
"data": {
"paymentResponse": {
"additionalData": {
"paymentMethod": "visa"
},
"amount": {
"currency": "EUR",
"value": 5424
},
"merchantReference": "SOME_MERCHANT_ID_",
"paymentMethod": {
"brand": "visa",
"type": "scheme"
},
"pspReference": "XXXX9XXXXXXXXX99",
"resultCode": "Authorised"
}
},
"errors": []
}
}
}
Performing additional actions (optional)
Optionally, additional actions may be required: authentication of payment with 3D Secure, scan of a QR code, or logging in to the bank to complete the payment. In this case, transactionProcess
mutation should be used.
mutation AdyenTransactionProcess($id: ID!, $data: JSON) {
transactionProcess(id: $id, data: $data) {
transaction {
id
actions
}
transactionEvent {
message
type
}
data
errors {
field
code
message
}
}
}
Where $data
is the object provided by Adyen Drop-in in the onAdditionalDetails
(Web, React Native), didProvide
(iOS) or makeDetailsCall
(Android) callback. The response is similar to the one from transactionInitialize
but the data
field has a different shape:
{
paymentDetailsResponse: PaymentDetailsResponse
errors?: SyncWebhookAppErrors;
}
PaymentDetailsResponse
is the result of calling Adyen's /payments/details
endpoint and is described in the Adyen documentation. SyncWebhookAppErrors
is described below.
If the errors
field doesn't exist or is an empty array, pass the paymentDetailsResponse
back to Adyen Drop-in. The Drop-in will handle the response and display the result to the user or again require additional actions to proceed.
Repeat the step until the payment is successful or fails.
Many payment methods are not settled synchronously. Sometimes it takes seconds, minutes, hours, or even days for a payment to go through. Adyen App will automatically handle Adyen webhook notifications and create transaction events in Saleor (see transactionEventReport
).
Apple Pay onValidateMerchant
To implement Apple Pay integration through Adyen and use your own Apple Pay certificate, you must implement onValidateMerchant
(Web, React Native) or onvalidatemerchant
(iOS). The Adyen Saleor App provides a way to validate the merchant using the paymentGatewayInitialize
mutation:
mutation PaymentGatewayInitialize($checkoutId: ID!, $data: JSON) {
paymentGatewayInitialize(
paymentGateways: [{ id: "app.saleor.adyen", data: $data }]
id: $checkoutId
) {
gatewayConfigs {
id
data
errors {
field
message
code
}
}
errors {
field
message
code
}
}
}
and provide the following JSON in $data
:
{
"action": "APPLEPAY_onvalidatemerchant",
"validationURL": "…",
"domain": "…",
"merchantIdentifier": "…",
"merchantName": "…"
}
All the parameters should be provided according to Apple Pay documentation on the Adyen website.
Additional endpoints (optional)
To use some payment methods inside Adyen Drop-in you may have to implement the following callbacks:
onBalanceCheck
(Web, React Native),checkBalance
(iOS) orcheckBalance
(Android)onOrderRequest
(Web, React Native),requestOrder
(iOS) orcreateOrder
(Android)onOrderCancel
(Web, React Native),cancelOrder
(iOS) orcancelOrder
(Android)
For example, these methods are required for Gift card split-payments.
Adyen's orders and requests for balance checks are not saved in Saleor. Each payment linked to Adyen's order will be stored as a separate transaction in Saleor.
Orders link transactions on the Adyen level. For example, if a user cancels an order (by removing gift card payment inside the Drop-in), every payment linked to that order will be refunded or voided.
onBalanceCheck
To call the /paymentMethods/balance
endpoint use the paymentGatewayInitialize
mutation:
mutation PaymentGatewayInitialize($checkoutId: ID!, $data: JSON) {
paymentGatewayInitialize(
paymentGateways: [{ id: "app.saleor.adyen", data: $data }]
id: $checkoutId
) {
gatewayConfigs {
id
data
errors {
field
message
code
}
}
errors {
field
message
code
}
}
}
and provide the following JSON in $data
:
{
"action": "checkBalance",
"paymentMethod": {
"type": "giftcard",
"brand": "givex",
"encryptedCardNumber": "...",
"encryptedSecurityCode": "..."
}
}
The contents of the paymentMethod
field come from the Adyen Drop-in.
Example onBalanceCheck
implementation in TypeScript could look like this (depending on your GraphQL client):
async onBalanceCheck(resolve, reject, data) {
const {
paymentGatewayInitialize: { gatewayConfigs },
} = await client.request(PaymentGatewayInitialize, {
checkoutId,
data: { action: "checkBalance", paymentMethod: data.paymentMethod },
});
const response = gatewayConfigs[0].data.giftCardBalanceResponse;
resolve(response);
}
Error handling is intentionally omitted for brevity.
The response received from Saleor with data from the Adyen app will be:
{
"data": {
"paymentGatewayInitialize": {
"gatewayConfigs": [
{
"id": "app.saleor.adyen",
"data": {
"giftCardBalanceResponse": {
"balance": {
"currency": "EUR",
"value": 5000
},
"pspReference": "BK4C…NN82",
"resultCode": "NotEnoughBalance"
}
}
}
]
}
}
}
onOrderRequest
Similarly to onBalanceCheck
, to call the /orders
endpoint use the paymentGatewayInitialize
mutation and pass the following $data
:
{
"action": "createOrder"
}
The onOrderCreate
implementation in TypeScript could look like this (depending on your GraphQL client):
async onOrderCreate(resolve, reject, data) {
const {
paymentGatewayInitialize: { gatewayConfigs },
} = await client.request(PaymentGatewayInitialize, {
id: checkoutId,
data: { action: "createOrder" },
});
const response = gatewayConfigs[0].data.orderCreateResponse;
resolve(response);
}
The response received from Saleor with data from the /orders
endpoint will be:
{
"data": {
"paymentGatewayInitialize": {
"gatewayConfigs": [
{
"id": "app.saleor.adyen",
"data": {
"orderCreateResponse": {
"amount": {
"currency": "EUR",
"value": 12000
},
"expiresAt": "2023-07-04T11:34:02Z",
"orderData": "...",
"pspReference": "W...82",
"reference": "5f0d76d5-aaed-40d3-87ff-bd34d6849f95,Q2hhbm5lbDoy,'c'",
"remainingAmount": {
"currency": "EUR",
"value": 12000
},
"resultCode": "Success"
}
}
}
]
}
}
}
Once the order is created, you may complete the payment. Use the data received from the Drop-in inside the onSubmit
method.
It includes the order
property - Adyen App will charge the customer with the amount specified in the order's data.
For example: if you have only 20 EUR left on a gift card, and the order's total amount is 50 EUR, order
data will firstly charge the gift card with the available amount (20 EUR) and the 2nd payment method with the outstanding order amount (50 - 20 = 30 EUR).
Adyen app uses a pspReference
field internally to link the notifications from
ORDER_CLOSED
Adyen webhook events
This field shouldn't be used by any external system, as it can change at any time without further notice.
onOrderCancel
Should be called when the user removes a payment method in a pending Adyen order, for example when there was already a partial charge for a gift card but the user decided to use a different payment method.
Canceling order in Adyen, automatically reports a new event on TransactionItem
with it's type
set to either REFUND_REQUEST
(when charge flow was used) or CANCEL_REQUEST
(when authorization flow was used). This is done in order to prevent order creation once the user requests a refund.
In case TransactionItem
status cannot be updated, a request to Adyen won't be sent in order to prevent fraudulent orders.
To call the /orders/cancel
endpoint, similarly to onBalanceCheck
, use the paymentGatewayInitialize
mutation and pass the following $data
:
{
"action": "cancelOrder",
"orderData": "..."
"pspReference": "..."
}
The response from that mutation will be:
{
"data": {
"transactionProcess": {
"data": {
"orderCancelResponse": {
"pspReference": "...",
"resultCode": "Received"
}
}
}
}
}
Example
The onOrderCancel
implementation in TypeScript could look like this (depending on your GraphQL client):
async onOrderCancel({order}) {
const {
paymentGatewayInitialize: { gatewayConfigs },
} = await client.request(PaymentGatewayInitialize, {
id: checkoutId,
data: {
action: "cancelOrder",
pspReference: order.pspReference,
orderData: order.orderData,
},
});
const response = gatewayConfigs[0].data.orderCancelResponse;
if (response.resultCode !== "Received") {
throw new Error("Cannot cancel order");
}
checkout.update({order: undefined});
}
Error handling
The three mutations described above may return data.errors
field. The existence of this field determines that the request was unsuccessful. errors
is an array of SyncWebhookAppError
objects. The SyncWebhookAppError
object has the following fields:
{
code?: string;
message?: string;
details?: JSONObject;
}
The code
field is a string identifying the error. One of the following values is allowed:
UnknownError
JsonSchemaError
MissingSaleorApiUrlError
MissingAuthDataError
- when app was incorrectly installed / uninstalled and token to communicate with Saleor cannot be foundHttpClientError
InvalidDataError
- whendata
field provided by storefront is invalid
This list may be extended in the future. Make sure your app handles unknown error codes.
The message
field is a human-readable message describing the error.
The details
field is an object containing additional information about the error. It will contain different fields, depending on error code.
Adyen API errors
Error details
will contain two fields:
errorCode
– Adyen error codestatusCode
– Adyen HTTP status code
Example:
{
"data": {
"transactionInitialize": {
"transactionEvent": {
"pspReference": "",
"amount": {
"amount": 54.24,
"currency": "EUR"
},
"type": "AUTHORIZATION_FAILURE"
},
"data": {
"errors": [
{
"code": "HttpClientError",
"message": "HTTP Exception: 422. : Unable to decrypt data",
"details": {
"errorCode": "174",
"statusCode": 422
}
}
],
"paymentResponse": {}
},
"errors": []
}
}
}
Incorrect input data
When making request with any action
described in "Additional endpoints" section, app might return errors
field with a list of payment methods, just like when making "Obtain Adyen payment methods" request, rather than performing the requested action. This might happen when data
requested by storefront was in invalid format.
In that case a single InvalidDataError
error will be included in errors
array with message
field that explains what went wrong and with details
field that contains fieldErrors
that describe error for each field required in the request:
{
"data": {
"paymentGatewayInitialize": {
"gatewayConfigs": [
{
"id": "app.saleor.adyen",
"data": {
"paymentMethodsResponse": {
"paymentMethods": [
{
"brands": ["visa", "mc"],
"name": "Credit Card",
"type": "scheme"
}
]
},
"clientKey": "test_AHSJKADHK12731KDSALD11DSADASA003",
"environment": "TEST"
},
"errors": [
{
"code": "InvalidDataError",
"message": "Invalid action data was provided in PaymentGatewayInitializeSession request: orderData: Required, pspReference: Required. Please check documentation for required parameters",
"details": {
"formErrors": [],
"fieldErrors": {
"orderData": [
"Required"
],
"pspReference": [
"Required"
]
}
}
}
]
}
],
"errors": []
}
}
}
Free orders
You may have use cases where you want to create free orders, such as free samples or free digital downloads. This may require skipping the payment step and completing the order without a transaction.
Adyen Drop-in integration does not support handling payments with a total amount of zero. Amount: 0, has a specific meaning in Adyen - it allows payment method tokenization or to obtain other details from the merchant database.