When a new online merchant chooses a payment gateway, one likely narrows them down to Adyen, Braintree, PayPal, Stripe. They're indeed the most popular today, and they are all robust and easy to integrate.
PayPal is unique, in that it's actually an integration to PayPal wallet, and credit cards, debit cards, ACH are just funding sources to the wallet. The integration model is via redirect.
Adyen, Braintree, and Stripe are more traditional card payment integrations. We will look at how the simplest integration works on each of them. The simplest yet super powerful integration is so-called web drop-in. It's an all-in-one web UI via Javascript that requires minimal or no customization; its server side implementation usually comes in SDK and only requires configuration.
Documentation of web drop-in is well written. Step-by-step guide is relatively accurate, with the exception of some side-track task which isn't explained well inside the guide (eg., originKey).
When it comes to the complex "additional actions" such as 3DS and 3DS 2.0, the client-side library has excellent generalization and abstraction to hide the complexity, easing developer's life by simply bridging server side response.action to the library's dropin.handleAction() function. It's a pass through, UI library interprets it and carries on the correct flow.
onSubmit: (state, dropin) => {
// Your function calling your server to make the `/payments` request
makePayment(state.data)
.then(response => {
if (response.action) {
// Drop-in handles the action object from the /payments response
dropin.handleAction(response.action);
} else {
// Your function to show the final result to the shopper
showFinalResult(response);
}
})
.catch(error => {
throw Error(error);
});
},
onAdditionalDetails: (state, dropin) => {
// Your function calling your server to make a `/payments/details` request
makeDetailsCall(state.data)
.then(response => {
if (response.action) {
// Drop-in handles the action object from the /payments response
dropin.handleAction(response.action);
} else {
// Your function to show the final result to the shopper
showFinalResult(response);
}
})
.catch(error => {
throw Error(error);
});
}
One thing to note is that to collect and transmit card data, Adyen only requires merchant's web page to load Adyen's Javascript which, in turn, loads the iFrame HTML, and CSS. There is no communication between the consumer's browser and Adyen otherwise. The Javascript encrypts the secure data and passes the ciphers into onSubmit() event, so the ciphers can be passed to server. Server then is responsible to invoke Adyen API, from server side.
In other words, the communication is between merchant's front-end and server side. This is a welcomed design, as the data flow is well controlled and funneled through merchant's server. The result is it's easier to log, trace and debug. Also in edge cases if consumer's environment has spotty connection with payment gateway, this approach eases the point of failures.
Here is an example of what kind of card data event gets and will send to server:
{
isValid: true,
data: {
paymentMethod: {
type: "scheme",
encryptedCardNumber: "adyenjs_0_1_18$k7s65M5V0KdPxTErhBIPoMPI8HlC..",
encryptedExpiryMonth: "adyenjs_0_1_18$p2OZxW2XmwAA8C1Avxm3G9UB6e4..",
encryptedExpiryYear: "adyenjs_0_1_18$CkCOLYZsdqpxGjrALWHj3QoGHqe+..",
encryptedSecurityCode: "adyenjs_0_1_18$XUyMJyHebrra/TpSda9fha978+.."
holderName: "S. Hopper"
}
}
}
Server takes data.paymentMethod, and calls
/payments
API, similar to:curl https://checkout-test.adyen.com/v52/payments \
-H 'x-api-key: YOUR_API_KEY' \
-H 'content-type: application/json' \
-d '{
"merchantAccount": "YourCompanyECOM",
"reference": "My first Adyen test payment",
"amount": {
"value": 1000,
"currency": "EUR"
},
"paymentMethod": {
"type": "scheme",
"encryptedCardNumber": "test_4111111111111111",
"encryptedExpiryMonth": "test_03",
"encryptedExpiryYear": "test_2030",
"encryptedSecurityCode": "test_737"
}
Of course, Adyen sends notifications of authorization status to merchant's webhooks asynchronously. Notifications can be treated as source of truth of transaction status.
"Get Started" is also well written, although to complete client and server sides, one may need to flip between 2 pages, unless one's preferred server is node.
Different from Adyen, Braintree client side sends secure card data to Braintree Server, securely, and gets a nonce back. This nonce (or token) represents the card user has entered, and can be used in payment API.
Thus, client needs to pass the nonce to merchant's server, and merchant's server invokes Braintree API with it. In order for Braintree to identify who the merchant is when it receives card data from client, the client needs to retrieve a client token from merchant's server at the beginning.
To generate the nonce, Braintree Javascript invokes Braintree GraphSQL API "TokenizeCreditCard" and passes in card data.
{
"clientSdkMetadata": {
"source": "client",
"integration": "dropin2",
"sessionId": "31f67c62-0659-4f28-84e7-3f80812aaa6d"
},
"query": "mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) { tokenizeCreditCard(input: $input) { token creditCard { bin brandCode last4 expirationMonth expirationYear binData { prepaid healthcare debit durbinRegulated commercial payroll issuingBank countryOfIssuance productId } } } }",
"variables": {
"input": {
"creditCard": {
"number": "4111111111111111",
"expirationMonth": "11",
"expirationYear": "2021"
},
"options": {
"validate": false
}
}
},
"operationName": "TokenizeCreditCard"
}
The response includes the nonce (token):
{
"data": {
"tokenizeCreditCard": {
"token": "tokencc_bf_9m54x2_z5jmpw_p7rx27_b3thkd_gw7",
"creditCard": {
"bin": "411111",
"brandCode": "VISA",
"last4": "1111",
"expirationMonth": "11",
"expirationYear": "2021",
"binData": {
"prepaid": "UNKNOWN",
"healthcare": "UNKNOWN",
"debit": "UNKNOWN",
"durbinRegulated": "UNKNOWN",
"commercial": "UNKNOWN",
"payroll": "UNKNOWN",
"issuingBank": null,
"countryOfIssuance": null,
"productId": null
}
}
}
},
"extensions": {
"requestId": "gmbVRWzzFpKtTai4bQXJuIHp81wJ5JyQhrn4sIqh_SkRUYwzpVdpYg=="
}
}
To invoke API from server side is simple using SDK, simile to this:
$result = $gateway->transaction()->sale([
'amount' => '10.00',
'paymentMethodNonce' => $nonceFromTheClient,
'deviceData' => $deviceDataFromTheClient,
'options' => [
'submitForSettlement' => True
]
]);
What's missing in the "Get Started" guide is how to handle verification such as 3DS and 3DS 2.0. The starter guide demonstrates the happy path only. In fact, 3DS verification requires additional customer data in addition to card data, therefore, is explained in a separate section of documentation.
It uses the same drop-in Javascript library but requires additional data to pass to Braintree. Once 3DS passes, client has a nonce just like normal transaction, plus an threeDSecureAuthenticationId. Either can be used by server to invoke Braintree API. (note as of May 20, 2020, the example using authentication ID is wrong, probably a result of copy and paste).
So, Braintree's integration enables communication to Braintree on both client and server side: client works with Braintree to tokenize card data, server uses the token to authorize payment. Verification flow such as 3DS and 3DS 2.0 in Braintree's implementation requires extra conscious handling and may introduce complexity in integration code.
Stripe has been taking on APIs from a quite different philosophy since long time ago. It builds APIs with abstraction similar to object-oriented programming, rather than treating them as resources and documents. Before, Stripe abstracts different payment methods as "charge"; the latest revamp, in order to encompass even more diverse payment methods globally, is called "Payment Intent".
Stripe, as many have already claimed, is the most developer friendly. Its documentation is detailed, accurate, and in the flow of a developer's mind, therefore, easy to follow.
Its "drop-in" implementation looks like this:
As we can see, Stripe's philosophy is heavy on client side. Server's involvements are:
The rest of interaction is all on client side, which server doesn't participate.
When client confirms payment to Stripe, Javascript invokes the /confirm API with payload in URLEncoded format with card data, something like this:
payment_method_data[type]=card&payment_method_data[billing_details][name]=Jenny+Rosen&payment_method_data[billing_details][address][postal_code]=11111&payment_method_data[card][number]=4111111111111111&payment_method_data[card][cvc]=111&payment_method_data[card][exp_month]=11&payment_method_data[card][exp_year]=21&payment_method_data[guid]=1111&payment_method_data[muid]=1111&payment_method_data[sid]=1111&payment_method_data[payment_user_agent]=stripe.js%2F659b1055%3B+stripe-js-v3%2F659b1055&payment_method_data[time_on_page]=35204&payment_method_data[referrer]=http%3A%2F%2Flocalhost%2Fstripe%2Findex.php&expected_payment_method_type=card&use_stripe_sdk=true&key=pk_test_1111&client_secret=pi_1111_secret_1111
A successful confirm response:
{
"id": "pi_1111",
"object": "payment_intent",
"amount": 1099,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"client_secret": "pi_1111_secret_1111",
"confirmation_method": "automatic",
"created": 1589994584,
"currency": "usd",
"description": null,
"last_payment_error": null,
"livemode": false,
"next_action": null,
"payment_method": "pm_1111",
"payment_method_types": [
"card"
],
"receipt_email": null,
"setup_future_usage": null,
"shipping": null,
"source": null,
"status": "succeeded"
}
The biggest advantage of heavy client side interaction is the ease of orchestrating user flow. For example, in 3DS flow, as we have seen with Adyen and Braintree, additional data or some form of special handling code are needed, however, with Stripe, it's all handled within client side library, and is transparent to merchants.
Here is what a confirm response looks like that requires 3DS, and client automatically handles "require_action" and "next_action" to render 3DS:
{
"id": "pi_1111",
"object": "payment_intent",
"amount": 1099,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"client_secret": "pi_1111_secret_1111",
"confirmation_method": "automatic",
"created": 1589994963,
"currency": "usd",
"description": null,
"last_payment_error": null,
"livemode": false,
"next_action": {
"type": "use_stripe_sdk",
"use_stripe_sdk": {
"type": "three_d_secure_redirect",
"stripe_js": "https://hooks.stripe.com/redirect/authenticate/src_1GkvWcEccjWVutyemmjLB0Vr?client_secret=src_client_secret_QhNk0j5vrKbK6leTQdijOAYQ",
"source": "src_1111",
"known_frame_issues": "false"
}
},
"payment_method": "pm_1111",
"payment_method_types": [
"card"
],
"receipt_email": null,
"setup_future_usage": null,
"shipping": null,
"source": null,
"status": "requires_action"
}
The drawback is that server is completely in the dark. Once a payment intent is created, client secret sent to client, the payment is in the hands of consumer and Stripe. In most of the cases, it actually should be this way - as long as Stripe, through webhooks, tells merchants details about the payments, why would merchants want to be in the middle of a payment?
In some rare cases where server may want to make a final decision whether to authorize this transaction, it cannot be done with this integration.
Although the primary factors of deciding a payment gateway may not likely be technical integration, it's insightful to understand the design philosophy of each integration. However, there are no right or wrong designs, nor best integration method, knowing the differences help merchants to design a better overall online payment experience.