paypal-checkout.js/README.md

641 lines
18 KiB
Markdown
Raw Permalink Normal View History

2021-10-13 05:08:07 +00:00
# [@root/paypal-checkout](https://git.rootprojects.org/root/paypal-checkout.js)
2021-10-09 23:10:40 +00:00
In contrast to the official PayPal Checkout SDK - which is auto-generated code
with lots of abstraction without much value - this is very little abstraction,
but specificially designed to be (mostly) idiomatic JavaScript / Node.js. \
(excuse the `snake_case` - that's how the PayPal REST API is designed).
![](https://i.imgur.com/brFTseM.png "PayPal Checkout API Flow")
<img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/checkout-logo-large.png" alt="Check out with PayPal" />
The Good Documentation™ for the PayPal API (a.k.a. PayPal Checkout SDK) is the
"REST API". See
- <https://developer.paypal.com/docs/api/orders/v2/> (one-time payments)
- <https://developer.paypal.com/docs/api/subscriptions/v1/> (recurring
subscriptions)
- <https://www.paypal.com/webapps/mpp/logos-buttons> (the buttons)
2021-10-17 06:51:12 +00:00
# Table of Contents
- Install
- QuickStart
- Orders (One-Time Purchases)
- PayPal Checkout Buttons
- REST API Overview
- Orders & Subscriptions
- Enums
2021-10-17 06:51:12 +00:00
- Redirects
- Webhooks
- Security & Notes
- Detailed Examples & Glossary
# Install
2021-10-09 23:10:40 +00:00
```bash
npm install --save @root/paypal-checkout
```
Optional, for Type Linting and Auto-Complete
```bash
npm install --save @root/paypal-checkout-product-categories
```
2021-10-17 06:51:12 +00:00
# Quick Start
If you just want to create a "Buy Now" or "Checkout with PayPal" type of button,
here's the gist of what you need to do:
## Order (One-Time Purchase)
1. Initialize the API
```js
"use strict";
require("dotenv").config({ path: ".env" });
let PPC = require("@root/paypal-checkout");
PPC.init(
process.env.PAYPAL_CLIENT_ID || "xxxx",
process.env.PAYPAL_CLIENT_SECRET || "****",
"sandbox" || "live",
{
// default query params for endpoints that use them
prefer: "return=representation",
total_required: true,
page_size: 20,
}
);
```
2. Create a "Buy Now" link (for Approval)
```js
// See https://developer.paypal.com/docs/api/orders/v2/#orders_create
2021-10-17 07:04:49 +00:00
let myApiUrl = "https://example.com";
let myOrderId = "local-db-id-for-user-purchasing-product";
2021-10-17 06:51:12 +00:00
let order = await PPC.Order.createRequest({
application_context: {
2021-10-17 07:04:49 +00:00
// What to show on PayPal's Pay Now page
2021-10-17 06:51:12 +00:00
brand_name: "Bliss via The Root Group, LLC",
shipping_preference: "NO_SHIPPING",
landing_page: "LOGIN",
user_action: PPC.Order.user_actions.PAY_NOW,
2021-10-17 07:04:49 +00:00
return_url: `${myApiUrl}/api/redirects/paypal-checkout/return`,
cancel_url: `${myApiUrl}/api/redirects/paypal-checkout/cancel`,
2021-10-17 06:51:12 +00:00
},
purchase_units: [
{
request_id: "default",
2021-10-17 07:04:49 +00:00
custom_id: myOrderId,
2021-10-17 06:51:12 +00:00
// shown in PayPal Checkout Flow UI
description: "1 year of pure Bliss",
// on the charge (credit card) statement
soft_descriptor: "Bliss",
amount: {
currency_code: "USD",
value: "10.00",
},
},
],
});
console.info(
"Approve URL:",
order.links.find(function (link) {
return "approve" === link.rel;
}).href
);
```
3. Handle the redirect: Verify & Capture
```js
app.get("/api/redirects/paypal-checkout/return", async function (req, res) {
let orderId = req.query.token;
// verify that the user has paid
await PPC.Order.details(orderId)
.then(async function (order) {
console.info("Deliver the Product to:", order.payer.email_address);
if ("APPROVED" !== order.status) {
throw new Error("spoofed redirect or cancelled order");
}
// take the money
let capture = await PPC.Order.capture(order.id, {
final_capture: true,
});
})
.catch(next);
});
```
4. [Set](https://developer.paypal.com/developer/applications) and Handle the
[`PAYMENT.CAPTURE.COMPLETED`, `PAYMENT.CAPTURE.REVERSED`, and `CUSTOMER.DISPUTE.CREATED`](https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/)
WebHooks
2021-10-17 06:51:12 +00:00
```js
2021-10-17 07:04:49 +00:00
// Set webhook at https://developer.paypal.com/developer/applications
// Descriptions at https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/
2021-10-17 06:51:12 +00:00
app.get("/api/webhooks/paypal-checkout/:secret", async function (req, res) {
let crypto = require("crypto");
let secret = process.env.PAYPAL_WEBHOOK_SECRET || "";
let guess = req.params.secret;
if (
!secret ||
secret.length !== guess.length ||
!crypto.timingSafeEqual(Buffer.from(guess), Buffer.from(secret))
) {
next(new Error("bad webhook secret value"));
return;
}
let event = req.body;
switch (event.event_type) {
case "PAYMENT.CAPTURE.COMPLETED":
{
let orderId = event.supplementary_data.related_ids.order_id;
let localDbId = event.custom_id;
console.info(
`Confirm that PayPal Order ${orderId} for ${localDbId} has been paid.`
);
}
break;
case "PAYMENT.CAPTURE.REVERSED":
{
// deduct from user's account
}
break;
case "CUSTOMER.DISPUTE.CREATED":
{
// TODO send email to merchant (myself) to check out the dispute
}
break;
case "CUSTOMER.DISPUTE.CREATED":
{
// TODO send email to merchant (myself) to review the dispute status
}
break;
2021-10-17 06:51:12 +00:00
default:
console.log("Ignoring", event.event_type);
res.json({ sucess: true });
return;
}
});
```
2021-10-17 06:51:12 +00:00
## PayPal Checkout Buttons
2021-10-09 23:10:40 +00:00
2021-10-17 06:51:12 +00:00
- <https://www.paypal.com/webapps/mpp/logos-buttons> <== THE ONE YOU WANT
- <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/checkout-logo-large.png" alt="Check out with PayPal" />
- <https://developer.paypal.com/docs/checkout/>
- <https://www.paypal.com/buttons/>
2021-10-09 23:10:40 +00:00
2021-10-17 07:04:49 +00:00
# API Overview
2021-10-09 23:10:40 +00:00
2021-10-17 07:04:49 +00:00
## Init
2021-10-17 06:51:12 +00:00
```js
PayPal.init(client_id, client_secret, "sandbox", defaults);
PayPal.request({ method, url, headers, json });
2021-10-09 23:10:40 +00:00
```
2021-10-17 06:54:00 +00:00
### No Dependencies Needed
2021-10-09 23:10:40 +00:00
2021-10-17 06:51:12 +00:00
If you'd like to keep your code super lightweight, you don't even need an SDK -
you can just use simple HTTP requests:
```js
let qs = require("querystring");
let request = require("@root/request");
let paypalApi = "https://api-m.sandbox.paypal.com";
async function PayPalRequest(
endpoint = "/v2/checkout/orders",
query = { page_size: 20 },
body = { purchase_units: [] },
requestId // optional id for certain requests
) {
let search = qs.stringify(query);
return await request({
url: `${paypalApi}${endpoint}?${search}`,
auth: {
user: process.env.PAYPAL_CLIENT_ID,
pass: process.env.PAYPAL_CLIENT_SECRET,
},
headers: {
"PayPal-Request-Id": requestId,
},
json: body,
}).then(function (resp) {
if (rsep.status < 200 || resp.status >= 300) {
throw new Error("BAD RESPONSE");
}
return resp.toJSON().body;
});
}
```
2021-10-17 07:04:49 +00:00
## Subscriptions (Recurring Payments)
2021-10-17 06:51:12 +00:00
See https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions
2021-10-09 23:10:40 +00:00
```txt
2021-10-12 08:25:56 +00:00
// Webhook 'event_type':
PayPal.Product.create({ ... }); // CATALOG.PRODUCT.CREATED
PayPal.Product.list();
PayPal.Product.details(id);
2021-10-12 08:25:56 +00:00
PayPal.Product.update(id, { description }); // CATALOG.PRODUCT.UPDATED
2021-10-12 08:25:56 +00:00
PayPal.Plan.create({ ... }); // BILLING.PLAN.CREATED
PayPal.Plan.list();
PayPal.Plan.details(id);
2021-10-12 08:25:56 +00:00
PayPal.Plan.update(id, { description }); // BILLING.PLAN.UPDATED
2021-10-12 08:25:56 +00:00
PayPal.Subscription.createRequest({ ... }); // BILLING.SUBSCRIPTION.CREATED
// subscription.links[rel="approve"].href // BILLING.SUBSCRIPTION.ACTIVATED
// PAYMENT.SALE.COMPLETED
PayPal.Subscription.details(id);
PayPal.Subscription.cancel(id, { reason });
```
2021-10-17 07:04:49 +00:00
## Orders (One-Time Payments)
```txt
// Webhook 'event_type':
PayPal.Order.createRequest({ purchase_units }); // CHECKOUT.ORDER.APPROVED
PayPal.Order.capture(id, { final_capture }); // PAYMENT.CAPTURE.COMPLETED
```
See also:
- <https://developer.paypal.com/docs/api/orders/v2/#orders_create>
- <https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit_request>
- <https://developer.paypal.com/docs/api/orders/v2/#definition-order_application_context>
## Enums (optional)
For assistance with Type Linting and Auto-Complete, all of the PayPal Checkout
enums (ALL CAPS strings that have a limit set of allow values such as `PAY_NOW`
and `CONTINUE`) are available in code.
They are defined like this:
```js
Order.user_actions = {
CONTINUE: "CONTINUE",
PAY_NOW: "PAY_NOW",
};
```
You can inspect them simply like this:
```js
console.log(Order.user_actions);
```
Here's the full list:
```js
PayPal.Order.intents;
PayPal.Order.user_actions;
PayPal.Order.shipping_preferences;
PayPal.Plan.intervals;
PayPal.Plan.tenures;
PayPal.Product.categories; // See note below
PayPal.Product.types;
PayPal.Subscription.actions;
PayPal.Subscription.payee_preferences;
PayPal.Subscription.payer_selections;
PayPal.Subscription.shipping_preferences;
```
The one exception is `PayPal.Product.categories` which provides only a limited
set of generic values for simple products and services if
`@root/paypal-checkout-product-categories` is not installed.
2021-10-17 07:04:49 +00:00
## Redirects
2021-10-12 07:43:47 +00:00
2021-10-16 23:26:04 +00:00
- `return_url`
- `cancel_url`
2021-10-17 07:04:49 +00:00
### `return_url`
2021-10-16 23:26:04 +00:00
**_Order_** and **_Subscription_** requests have a return `return_url` will be
called with some or all of the following params:
2021-10-12 07:43:47 +00:00
```txt
# Order
https://example.com/redirects/paypal-checkout/return
?token=XXXXXXXXXXXXXXXXX
&PayerID=XXXXXXXXXXXXX
```
- `token` is the **_Order ID_**
- `PayerID` is... exactly what it seems (no idea how you can access the Payer
object though)
```txt
# Subscrption
2021-10-12 07:43:47 +00:00
https://example.com/redirects/paypal-checkout/return
?subscription_id=XXXXXXXXXXXXXX
&ba_token=BA-00000000000000000
&token=XXXXXXXXXXXXXXXXX
```
- `subscription_id` refers to both the **_Subscription ID_** and the
`billing_agreement_id` of the corresponding **_Payments_**.
- `ba_token` (deprecated) refers to `/v1/payments/billing-agreements/:ba_token`
- `token` refers to the **_Order ID_** (perhaps created as part of the setup fee
or first billing cycle payment).
2021-10-17 07:04:49 +00:00
### `cancel_url`
2021-10-16 23:26:04 +00:00
The `cancel_url` will have the same query params as the `return_url`.
Also, PayPal presents the raw `cancel_url` and will NOT update the order or
subscription status. It's up to you to confirm with the user and change the
status to `CANCELLED`.
2021-10-16 23:26:04 +00:00
2021-10-17 07:04:49 +00:00
## Webhooks
Webhooks can be set up in the Application section of the Dashboard:
- <https://developer.paypal.com/developer/applications>
You'll see a list of applications. Click on one to access the webhooks.
**Security**: You must put a `secret` or `token` or your webhook URLs - PayPal
provides no measure of authentication (and otherwise an attacker could just send
random crap to your webhooks making it look like they've paid for all sorts of
things).
# Security
#### User email addresses
Emails addresses available through the PayPal Checkout API guaranteed to have
been verified by PayPal.
See:
- [Is `resource.subscriber.email_address` verified by PayPal?](https://twitter.com/paypaldev/status/1448238655743488008)
- [How do I receive money through PayPal?](https://www.paypal.com/us/smarthelp/article/how-do-i-receive-money-through-paypal-faq1750)
# Notes
My discussions with Twitter Support (@paypaldev):
- <https://twitter.com/search?q=(from%3Acoolaj86)%20(to%3Apaypaldev)&src=typed_query>
2021-10-09 23:10:40 +00:00
Note: Just about everything in the PayPal SDK that uses `ALL_CAPS` is a
`constant`/`enum` representing an option you can pick from limited number of
options.
Sandbox accounts (for creating fake purchases) can be managed at:
<https://developer.paypal.com/developer/accounts>
## Auth vs Capture
2021-10-09 23:10:40 +00:00
> Authorization and capture enables you to authorize fund availability but delay
> fund capture. This can be useful for merchants who have a delayed order
> fulfillment process. Authorize & Capture also enables merchants to change the
> original authorization amount in case the order changes due to shipping,
> taxes, or gratuity.
>
> For any payment type, you can capture less than or the full original
> authorized amount. You can also capture up to 115% of or $75 USD more than the
> original authorized amount, whichever is less.
>
> See
>
> - <https://developer.paypal.com/docs/admin/auth-capture/>
> - <https://developer.paypal.com/docs/api/payments/v2/#authorizations_capture>
You can auth once and capture multiple times (unless you set `final_capture`).
2021-10-17 06:51:12 +00:00
# Examples
2021-10-09 23:10:40 +00:00
2021-10-17 06:51:12 +00:00
### Subscription.createRequest({ ... })
See
https://developer.paypal.com/docs/subscriptions/integrate/#use-the-subscriptions-api
```js
await Subscription.createRequest({
plan_id: plan.id,
//start_time: "2018-11-01T00:00:00Z", (must be in the future)
//quantity: "20",
//shipping_amount: { currency_code: "USD", value: "10.00" },
subscriber: {
name: { given_name: "James", surname: "Doe" },
email_address: "customer@example.com",
/*
shipping_address: {
name: { full_name: "James Doe" },
address: {
address_line_1: "123 Sesame Street",
address_line_2: "Building 17",
admin_area_2: "San Jose",
admin_area_1: "CA",
postal_code: "95131",
country_code: "US",
},
},
*/
},
application_context: {
brand_name: "Bliss via The Root Group, LLC",
locale: "en-US",
shipping_preference: Subscription.shipping_preferences.NO_SHIPPING,
user_action: Subscription.actions.SUBSCRIBE_NOW,
payment_method: {
payer_selected: Subscription.payer_selections.PAYPAL,
payee_preferred:
Subscription.payee_preferences.IMMEDIATE_PAYMENT_REQUIRED,
},
return_url:
"https://example.com/api/paypal-checkout/return?my_token=abc123",
cancel_url:
"https://example.com/api/paypal-checkout/cancel?my_token=abc123",
},
});
console.info("Subscription (Before Approval):");
console.info(JSON.stringify(subscription, null, 2));
console.info();
console.info(
"Approve URL:",
subscription.links.find(function (link) {
return "approve" === link.rel;
}).href
);
```
# Glossary
## Webhook Event: CHECKOUT.ORDER.APPROVED
```json
{
"id": "WH-1V203642KU442722T-3S346483MF8733038",
"event_version": "1.0",
"create_time": "2021-10-17T05:04:22.404Z",
"resource_type": "checkout-order",
"resource_version": "2.0",
"event_type": "CHECKOUT.ORDER.APPROVED",
"summary": "An order has been approved by buyer",
"resource": {
"create_time": "2021-10-17T05:03:26Z",
"purchase_units": [
{
"reference_id": "{purchase-unit-id}",
"amount": {
"currency_code": "USD",
"value": "10.00"
},
"payee": {
"email_address": "sb-a9xvi8075587@business.example.com",
"merchant_id": "4RXRQC77UD53U",
"display_data": {
"brand_name": "Bliss via The Root Group, LLC"
}
},
"description": "1 year of pure Bliss",
"custom_id": "{my-local-db-purchase-id}",
"soft_descriptor": "Bliss"
}
],
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F/capture",
"rel": "capture",
"method": "POST"
}
],
"id": "4K5112848U951142F",
"intent": "CAPTURE",
"payer": {
"name": {
"given_name": "John",
"surname": "Doe"
},
"email_address": "sb-ka5d18075586@personal.example.com",
"payer_id": "YTENGYR8PAF9A",
"address": {
"country_code": "US"
}
},
"status": "APPROVED"
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1V203642KU442722T-3S346483MF8733038",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1V203642KU442722T-3S346483MF8733038/resend",
"rel": "resend",
"method": "POST"
}
]
}
```
## Webhook Event: PAYMENT.CAPTURE.COMPLETED
```json
{
"id": "WH-3UT90572MR669760L-7LL94124G5389840D",
"event_version": "1.0",
"create_time": "2021-10-17T05:05:03.389Z",
"resource_type": "capture",
"resource_version": "2.0",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"summary": "Payment completed for $ 10.0 USD",
"resource": {
"amount": {
"value": "10.00",
"currency_code": "USD"
},
"seller_protection": {
"dispute_categories": ["ITEM_NOT_RECEIVED", "UNAUTHORIZED_TRANSACTION"],
"status": "ELIGIBLE"
},
"supplementary_data": {
"related_ids": {
"order_id": "4K5112848U951142F"
}
},
"update_time": "2021-10-17T05:04:29Z",
"create_time": "2021-10-17T05:04:29Z",
"final_capture": true,
"seller_receivable_breakdown": {
"paypal_fee": {
"value": "0.84",
"currency_code": "USD"
},
"gross_amount": {
"value": "10.00",
"currency_code": "USD"
},
"net_amount": {
"value": "9.16",
"currency_code": "USD"
}
},
"custom_id": "{my-local-db-purchase-id}",
"links": [
{
"method": "GET",
"rel": "self",
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5VK462069F664902F"
},
{
"method": "POST",
"rel": "refund",
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5VK462069F664902F/refund"
},
{
"method": "GET",
"rel": "up",
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F"
}
],
"id": "5VK462069F664902F",
"status": "COMPLETED"
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3UT90572MR669760L-7LL94124G5389840D",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3UT90572MR669760L-7LL94124G5389840D/resend",
"rel": "resend",
"method": "POST"
}
]
}
```