This post provides guidance on PayPal integration in the Power Pages site. It was inspired by a few requests on this topic, so I’ve decided to publish this guide.
Microsoft Power Pages evolve fast, and the Stripe integration is now available out of the box. I wrote a post about the Anatomy of the Stripe payments in Power Pages and How to improve Stripe payments in Power Pages. Check those out if you are interested in this topic. This post will focus on how to add PayPal to your Power Pages site, as it’s still not available natively.
Please note that some approaches demonstrated here are incomplete for the production level of use and don’t cover all important aspects (e.g. error handling).
Also, this article covers a payment page implementation demo and is not a complete e-commerce or service flow. For example, the payment amount is just hardcoded for demo purposes, and there is no receipt page, etc.
With these excuses said, let’s jump into the actual implementation, and we will start with the architecture.
If your business accepts payments from customers using credit cards, the last thing you want is to store or process the actual card details. Payment gateways are the businesses that outsource the risks and compliance for you and provide applications and infrastructure to manage payments on your behalf.
The typical high-level architecture involves 4 key participants: a customer, a merchant, a bank and a payment gateway:
The interactions between them are typically as follows:
The exact sequence of the above steps and details of the information exchanged vary from one payment gateway to another, while the pattern generally applies for the most.
Now, let’s look at how to specifically implement PayPal on Power Pages!
PayPal offers Checkout integration for processing online payments. This option combines client code (JavaScript for a browser) and server-side logic. The sequence diagram for such integration is listed on the PayPal documentation page.
As we will interact with PayPal Order APIs, we need to authenticate and get an access token – see the Authentication section on the PayPal API documentation. This requires a client ID and a client secret values. Obviously, no secret shall be stored and passed from the browser, so the connectivity with the API needs to be established on the server side.
Also, assuming that we want to write the payment transaction details to Dataverse, we need a secure place to do so. While the Power Pages Web API allows to connect securely from a browser under the Power Pages session, this is not a good option for the payments architecture, as explained in the previous section. For example, it could be vulnerable to tampering with the transaction details (e.g. amount).
We have several options for implementing the server-side component, but in this post, I want to demonstrate how the Power Platform can achieve this with a low-code approach. Let’s consider these options and their pros and cons:
So, I’ll use the Power Automate cloud flow integration from Power Pages for this demo.
The specific architecture of this implementation is pictured below. Can you recognise the corresponding components from the high-level diagram earlier? (Hint: there is no Bank on this one).
To implement this architecture we will need:
This guide assumes some basic knowledge of the Power Platform: working with solutions, cloud flows, environment variables etc. The screenshots are provided where required to clear the ambiguity or highlight critical points.
1. Sign up for a PayPal developer account if you don’t have one yet.
2. In the PayPal Developer Dashboard, on the Apps & Credentials tab, create a new REST API application. Give it a name of your choice, and you will receive a client ID and a client secret needed later to authenticate the API calls. So, keep the browser tab open.
3. Go to the Power Pages maker environment https://make.powerpages.microsoft.com/ and in the Solutions section create a new solution and give it a name of your choice, e.g. PayPal Integration
.
4. Create 2 environment variables:
PAYPAL_CLIENT_ID
– and set the current value of Client ID from PayPal REST API app created at step 2.PAYPAL_CLIENT_SECRET
– the value of Secret from the REST API app.NOTE: For production use case you should use the “secret” type for the environment variable type. This would require an Azure account with the Secrets vault service created in it.
5. Go to the Power Pages Studio and select the Data section on the left. You will see a settings cog near the Data label as shown by an arrow on the screenshot below. Click that cog and select the solution you’ve just created (e.g. PayPal Integration).
In this section we will do a few things:
6. Log in to the Power Pages maker portal and create a new site (unless you have one already).
7. Create a new page on the Power Pages site and name it Order Payment or similar.
8. Go to the Power Pages Management app, accessible from the ellipsis menu “…” just below the Set up section:
9. Create a content snippet in the Portal Management app (select type HTML). See the Power Pages documentation for more details if needed.
10. Copy the following code into the content of the snippet. It does two things:
<div>
tag creates an HTML structure and loads the form via the external script.<script>
tag is a basic placeholder function to render the form and PayPal buttons. Note that the createOrder()
and onApprove()
functions are stubs and will be filled in on the next steps. Check that the PayPal buttons appear on the Payment page.<div>
<div id="paypal-button-container" style="width: 350px;"></div>
<p id="result-message"></p>
<!-- Replace for production the "test" client-id value with your client-id -->
<script src="https://www.paypal.com/sdk/js?client-id=test¤cy=USD"></script>
</div>
<script language="JavaScript">
if (window.top != window.self){
window.paypal.Buttons({
async createOrder() {},
async onApprove(data, actions) {}
})
.render("#paypal-button-container");
function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;
}
}
</script>
11. In the Power Pages Management app, in the section Web Pages find the Order Payment page you created. In the Localized content section you should see another record calls Order payment which represents the localised version of the page and this is the one we need to put the content on – click on it.
12. Add the following code to the page and note the use of the content snippet you’ve just created (Liquid markup):
<div id="i3jhx6" class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
<div id="ibdmvz" class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">
<div id="iykqyu" class="col-md-4 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 250px; word-break: break-word;"><img src="/Cat-PC.png" id="i8dr8i" alt="Cat-PC" name="Cat-PC.png" style="max-width: 100%; object-fit: contain; margin: 0 50px 0 0;" /></div>
<div id="i0sicq" class="col-md-8 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 250px; word-break: break-word;">
<p id="i0kgrm">Payment</p>
{{ snippets["PayPal payment"] }}
</div>
</div>
</div>
If everything has been done correctly so far, you should see the page with a cat and two PayPal buttons as below. (You would need to login to see that page as we set it up authenticated).
You have 2 options for implementing the Dataverse table to capture payment transaction details.
If you already using the Power Pages Stripe integration (in Preview) or if you want to have a solution compatible with the payments solutions for Power Pages from Microsoft, you may want to use the Payments table that comes when this feature is enabled. It comes as part of Stripe integration, but don’t worry, you don’t need to have or use Stripe to enable the Payment table. It’s not payment gateway specific and can be extended nicely as explained below.
Follow these steps to enable it:
13a. In the Power Pages Studio select Set up section on the left panel and then find the External apps (preview) section.
14a. You will see 2 integrations available – DocuSign and Stripe. We are interested in the latter today, so select Install in the Stripe row below. Click Start installation in the pop up dialog to confirm and wait until it completes.
15a. Go to your solution created at step 8 and Add existing -> Table and select the Payment table that has been added to your environment.
16a. In the same solution, use Add existing -> More -> Choice and select Provider choice. Customize the choice by adding a new provider – PayPal.
This is it, you don’t need to do anything else. No need to setup Stripe keys as you age not going to use it. Move to the section Create Order flow and script.
If you don’t want to get bound by the additional component in your solution, you can create your own payments page. I recommend the columns for the table as outlined below for compatibility with the rest of the guide.
Follow these steps to create the table:
13b. Create a new Table and call it Payment, leaving everything else by default.
14b. Add the following additional columns to it with the type specified:
Amount
: CurrencyPayment date time:
Date & timePayment identifier
: StringPayment method
: StringPayment status
: Choice of Created, Succeeded or FailedPayment status reason
: StringProvider
: Choice of PayPal and OtherNow we have the required components to create the first step in the payment flow: create a PayPal Order object and initiate a new record in the Payment table as not yet completed payment.
This is an overview of the flow steps described below. You can find the solution (unmanaged) with all flows and other components in this GitHub repository.
17. Create a new Instant Power Automate flow.
amount
. 18. Add the following actions:
Authenticate request
https://api-m.sandbox.paypal.com/v1/oauth2/token
"Content-Type": "application/x-www-form-urlencoded"
grant_type=client_credentials
PAYPAL_CLIENT_ID
PAYPAL_CLIENT_SECRET
access_token
for the next step)
Parse Authenticate response
@{body('Authenticate_request')}
{
"type": "object",
"properties": {
"scope": {
"type": "string"
},
"access_token": {
"type": "string"
},
"token_type": {
"type": "string"
},
"app_id": {
"type": "string"
},
"expires_in": {
"type": "integer"
},
"nonce": {
"type": "string"
}
}
}
Create Order request
https://api-m.sandbox.paypal.com/v2/checkout/orders
"Content-Type": "application/json"
Bearer @{body('Parse_JSON')?['access_token']}
{
"intent": "CAPTURE",
"purchase_units": [
{
"amount": {
"currency_code": "USD",
"value": @{triggerBody()['number']}
}
}
],
"payment_source": {
"paypal": {
"experience_context": {
"payment_method_preference": "IMMEDIATE_PAYMENT_REQUIRED",
"brand_name": "EXAMPLE INC",
"locale": "en-US",
"shipping_preference": "NO_SHIPPING",
"user_action": "PAY_NOW"
}
}
}
}
@{body('Create_Order_request')}
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"payment_source": {
"type": "object",
"properties": {
"paypal": {
"type": "object",
"properties": {}
}
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"href": {
"type": "string"
},
"rel": {
"type": "string"
},
"method": {
"type": "string"
}
},
"required": [
"href",
"rel",
"method"
]
}
}
}
}
@{triggerBody()['number']}
@{utcNow()}
@{body('Parse_Create_Order_response')?['id']}
order_id
@body('Parse_Create_Order_response')?['id']
We are done with the first flow – make sure to save it!
19. Register the created flow on the Power Pages site. Go to the Design Studio of the website, section Set up and select Cloud flows. Select the button Add existing flow:
20. In the pop up panel on the right select the Create Order flow and add a web role allowed to access this flow: Authenticated Users and click Add. Here is our security for the flow endpoints out of the box!
21. Once the flow is added, a specific endpoint URL will be assigned. Copy it to the clipboard.
22. In the Portal Management app, create a Settings record with the name “Payments/PayPal/CreateOrderURL” and the value of the power automate flow endpoint from the previous step, e.g.: /_api/cloudflow/v1.0/trigger/9397822b-dfdf...
23. In the same Portal Management app, go to Content Snippets, find the one you created earlier and in the snippet content replace the line async createOrder() {},
with the following block:
async createOrder() {
const url = "{{ settings["Payments/PayPal/CreateOrderURL"] }}";
const payload = {
eventData: JSON.stringify({
amount: 100
})
};
var orderId;
await shell
.ajaxSafePost({
type: "POST",
contentType: "application/json",
url: url,
data: JSON.stringify(payload),
processData: false,
global: false,
})
.done(function (response) {
const orderData = JSON.parse(response);
console.log(response);
if (orderData.order_id) {
orderId = orderData.order_id;
} else {
const errorDetail = orderData.details[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
}
})
.fail(function () {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
});
return orderId;
},
What’s happening here is a call to the Power Automate cloud flow Create Order created at the previous step and results handler, including:
eventData
and JSON.strigify()
24. Now, you can Sync and Preview the Payment page. What you should expect as a result of it, when the page loads you will see the two PayPal buttons (as before), but when you click on it, a card details and billing information section will appear.
25. Check the Create Order flow history and make sure that this flow has run successfully or fix errors if there were any.
Should you get lost with the steps or code changes, you can find the complete solution with Power Automate flow and script for the snippet in this GitHub repository.
Now, we can finalise the implementation by handling the capture payment event when a user submits credit card details.
26. Create a new Instant Power Automate flow:
Capture Order
and Skip the trigger selection at that step.order_id
27. Add the following actions to the flow:
Authenticate request
https://api-m.sandbox.paypal.com/v1/oauth2/token
"Content-Type": "application/x-www-form-urlencoded"
grant_type=client_credentials
PAYPAL_CLIENT_ID
PAYPAL_CLIENT_SECRET
@{body('Authenticate_request')}
{
"type": "object",
"properties": {
"scope": {
"type": "string"
},
"access_token": {
"type": "string"
},
"token_type": {
"type": "string"
},
"app_id": {
"type": "string"
},
"expires_in": {
"type": "integer"
},
"nonce": {
"type": "string"
}
}
}
Capture Order request
https://api-m.sandbox.paypal.com/v2/checkout/orders/@{triggerBody()['text']}/capture
"Content-Type": "application/json"
Bearer @{body('Parse_JSON')?['access_token']}
{}
@{body('Capture_Order_request')}
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"payment_source": {
"type": "object",
"properties": {
"paypal": {
"type": "object",
"properties": {
"email_address": {
"type": "string"
},
"account_id": {
"type": "string"
},
"account_status": {
"type": "string"
},
"name": {
"type": "object",
"properties": {
"given_name": {
"type": "string"
},
"surname": {
"type": "string"
}
}
},
"address": {
"type": "object",
"properties": {
"country_code": {
"type": "string"
}
}
}
}
}
}
},
"purchase_units": {
"type": "array",
"items": {
"type": "object",
"properties": {
"reference_id": {
"type": "string"
},
"payments": {
"type": "object",
"properties": {
"captures": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"amount": {
"type": "object",
"properties": {
"currency_code": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"final_capture": {
"type": "boolean"
},
"seller_protection": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"seller_receivable_breakdown": {
"type": "object",
"properties": {
"gross_amount": {
"type": "object",
"properties": {
"currency_code": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"paypal_fee": {
"type": "object",
"properties": {
"currency_code": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"net_amount": {
"type": "object",
"properties": {
"currency_code": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"href": {
"type": "string"
},
"rel": {
"type": "string"
},
"method": {
"type": "string"
}
},
"required": [
"href",
"rel",
"method"
]
}
},
"create_time": {
"type": "string"
},
"update_time": {
"type": "string"
}
},
"required": [
"id",
"status",
"amount",
"final_capture",
"seller_protection",
"seller_receivable_breakdown",
"links",
"create_time",
"update_time"
]
}
}
}
}
},
"required": [
"reference_id",
"payments"
]
}
},
"payer": {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"given_name": {
"type": "string"
},
"surname": {
"type": "string"
}
}
},
"email_address": {
"type": "string"
},
"payer_id": {
"type": "string"
},
"address": {
"type": "object",
"properties": {
"country_code": {
"type": "string"
}
}
}
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"href": {
"type": "string"
},
"rel": {
"type": "string"
},
"method": {
"type": "string"
}
},
"required": [
"href",
"rel",
"method"
]
}
}
}
}
pp_paymentidentifier eq '@{triggerBody()['text']}'
@{first(outputs('List_rows')?['body/value'])?['pp_paymentid']}
@{body('Parse_JSON')?['purchase_units']?[0]?['payments']?['captures']?[0]?['id']}
@{body('Parse_JSON')?['status']}
status_code
@outputs('Capture_Order_request')['statusCode']
order_data
@{body('Capture_Order_request')}
28. Register the created flow on the Power Pages site. Go to the Design Studio of the website, section Set up and select Cloud flows. Select the button Add existing flow and in the pop up panel on the right select the Capture Order flow and add a web role allowed to access this flow: Authenticated Users and click Add.
29. In the Portal Management app, create a Settings record with the name “Payments/PayPal/CaptureOrderURL” and the value of the power automate flow endpoint from the previous step, e.g.: /_api/cloudflow/v1.0/trigger/9397822b-dfdf...
30. Go to the content snippet in the Portal Management app, find the content snippet you created and changed previously and replace the line async onApprove(data, actions) {}
with the following block:
async onApprove(data, actions) {
const url = "{{ settings["Payments/PayPal/CaptureOrderURL"] }}";
const payload = {
eventData: JSON.stringify({
order_id: data.orderID
})
};
await shell
.ajaxSafePost({
type: "POST",
contentType: "application/json",
url: url,
data: JSON.stringify(payload),
processData: false,
global: false,
})
.done(function (response) {
console.log(`Capture response: ${response}`);
const flowResponse = JSON.parse(response);
const orderData = JSON.parse(flowResponse.order_data);
console.log(`Order data: ${orderData}`);
if (orderData.details) {
const errorDetail = orderData.details[0];
if (errorDetail.issue === "INSTRUMENT_DECLINED") {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else {
// (2) Other non-recoverable errors -> Show a failure message
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
};
};
if (!orderData.purchase_units)
throw new Error(JSON.stringify(orderData));
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
const transaction =
orderData.purchase_units[0].payments.captures[0] ||
orderData.purchase_units[0].payments.authorizations[0];
resultMessage(
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2),
);
})
.fail(function () {
console.error(error);
resultMessage(`Could not capture PayPal order...<br><br>${error}`);
});
}
hat’s happening here is a call to the Power Automate cloud flow Capture Order created at the previous step and results handler, including:
JSON.strigify()
and order_id: data.orderID
Time for the final end-to-end test!
The test is now to confirm that the transaction is successfully recorded on both PayPal and Dataverse side and the Payments table is updated with the PayPal transaction information.
31. Reload the payment page, click the Debit or Credit Card button and, after a small delay, a section to enter card and billing details will appear, as before. Enter the test card details below and click Pay now. You can find more testing card numbers for different scenarios on PayPal website.
2223000048400011
12/28
123
You should see a successful message after some delay and the browser console log will show you the API response details:
32. Once the transaction is successful and a message is displayed, open your PayPal developer dashboard, Event logs section https://developer.paypal.com/dashboard/dashboard/sandbox. You should see there 2 records – one event from the Create Order flow when the payment section was open and another one when you clicked Pay Now.
33. Return to Power Pages Studio, select Data section and select the Payments table. You should see a row or few. Sort by the column Payment date time to see the latest transaction and add the columns Payment details, Payment identifier and Payment status if not selected yet. You should see the transaction details.
Well, this post turned out to be much longer than I expected and would like it to be. But it dives into many details and aims to give you a big picture. I hope you found it useful, please let me know if it did!
The solution built using this guide is in no way complete. To bring this to your actual site you need to take care of at least the following:
I hope this guide helped you to start on this journey and provided a demonstration of Power Automate cloud flows integration in Power Pages. Please let me know what you want to know more! Please comment or reach out to me any time at andrew@technomancy.com.au.
Original Post https://cloudminded.blog/2024/03/06/power-pages-paypal-integration/