npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@root/paypal-checkout

v0.3.1

Published

A more sensible, human-generated wrapper for the PayPal Checkout REST API

Downloads

6

Readme

@root/paypal-checkout

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

The Good Documentation™ for the PayPal API (a.k.a. PayPal Checkout SDK) is the "REST API". See

Table of Contents

  • Install
  • QuickStart
    • Orders (One-Time Purchases)
    • PayPal Checkout Buttons
  • REST API Overview
    • Orders & Subscriptions
    • Enums
    • Redirects
    • Webhooks
  • Security & Notes
  • Detailed Examples & Glossary

Install

npm install --save @root/paypal-checkout

Optional, for Type Linting and Auto-Complete

npm install --save @root/paypal-checkout-product-categories

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

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

    // See https://developer.paypal.com/docs/api/orders/v2/#orders_create
    let myApiUrl = "https://example.com";
    let myOrderId = "local-db-id-for-user-purchasing-product";
    let order = await PPC.Order.createRequest({
      application_context: {
        // What to show on PayPal's Pay Now page
        brand_name: "Bliss via The Root Group, LLC",
        shipping_preference: "NO_SHIPPING",
        landing_page: "LOGIN",
        user_action: PPC.Order.user_actions.PAY_NOW,
        return_url: `${myApiUrl}/api/redirects/paypal-checkout/return`,
        cancel_url: `${myApiUrl}/api/redirects/paypal-checkout/cancel`,
      },
      purchase_units: [
        {
          request_id: "default",
          custom_id: myOrderId,
          // 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

    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 and Handle the PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REVERSED, and CUSTOMER.DISPUTE.CREATED WebHooks

    // Set webhook at https://developer.paypal.com/developer/applications
    // Descriptions at https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/
    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;
        default:
          console.log("Ignoring", event.event_type);
          res.json({ sucess: true });
          return;
      }
    });

PayPal Checkout Buttons

API Overview

Init

PayPal.init(client_id, client_secret, "sandbox", defaults);
PayPal.request({ method, url, headers, json });

No Dependencies Needed

If you'd like to keep your code super lightweight, you don't even need an SDK - you can just use simple HTTP requests:

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

Subscriptions (Recurring Payments)

See https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions

                                              // Webhook 'event_type':

PayPal.Product.create({ ... });               // CATALOG.PRODUCT.CREATED
PayPal.Product.list();
PayPal.Product.details(id);
PayPal.Product.update(id, { description });   // CATALOG.PRODUCT.UPDATED

PayPal.Plan.create({ ... });                  // BILLING.PLAN.CREATED
PayPal.Plan.list();
PayPal.Plan.details(id);
PayPal.Plan.update(id, { description });      // BILLING.PLAN.UPDATED

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

Orders (One-Time Payments)

                                                // Webhook 'event_type':

PayPal.Order.createRequest({ purchase_units }); // CHECKOUT.ORDER.APPROVED
PayPal.Order.capture(id, { final_capture });    // PAYMENT.CAPTURE.COMPLETED

See also:

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:

Order.user_actions = {
  CONTINUE: "CONTINUE",
  PAY_NOW: "PAY_NOW",
};

You can inspect them simply like this:

console.log(Order.user_actions);

Here's the full list:

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.

Redirects

  • return_url
  • cancel_url

return_url

Order and Subscription requests have a return return_url will be called with some or all of the following params:

# 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)
# Subscrption
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).

cancel_url

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.

Webhooks

Webhooks can be set up in the Application section of the Dashboard:

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:

Notes

My discussions with Twitter Support (@paypaldev):

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

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

You can auth once and capture multiple times (unless you set final_capture).

Examples

Subscription.createRequest({ ... })

See https://developer.paypal.com/docs/subscriptions/integrate/#use-the-subscriptions-api

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: "[email protected]",
    /*
      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

{
  "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": "[email protected]",
          "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": "[email protected]",
      "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

{
  "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"
    }
  ]
}