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

@instant.dev/payments

v0.0.7

Published

Payments library that uses Stripe as a system of record

Downloads

21

Readme

Instant Payments

travis-ci build npm version

Use Stripe as a System of Record

We built Instant Payments because Price discovery in SaaS is difficult: pricing and plans often take multiple iterations. Having to redo your billing system every time you get a new piece of information about price points wastes time you could be spending on your product.

Instant Payments provides a simple abstraction on top of Stripe that simplifies everything to just two .json files representing Plans and LineItems, which provide abstractions over Stripe's own Products, Prices, Subscriptions and SubscriptionItems. Instant Payments then provides an easy-to-use subscribe() and unsubscribe() method:

import InstantPayments from '@instant.dev/payments';
const payments = new InstantPayments(
  process.env.STRIPE_SECRET_KEY,
  process.env.STRIPE_PUBLISHABLE_KEY,
  `./_instant/payments/cache/stripe_plans.json` // more on this cached file below
);

let subscription = await payments.customers.subscribe({
  email,
  planName: 'business_plan',
  lineItemCounts: {
    collaborator_seats: 100,
    projects: 0,
    environments: 0,
    linked_apps: 0,
    hostnames: 0
  },
  successURL: `/success/`,
  cancelURL: `/fail/`
});

This will automatically configure a Subscription with relevant SubscriptionItems and create a Stripe Checkout session for your customer which you can then direct them to on the front end.

No database or webhooks? How does it work?

Instant Payments makes a few core assumptions to make working with Stripe easier;

  • Each user is identified by a unique email (tip: use tags, e.g. [email protected])
  • You do not need a custom checkout implementation; Stripe Checkout is acceptable
  • Only USD is supported at the moment (may expand scope)

If you can work within these limitations, webhooks can be avoided completely by using Stripe's hosted checkout for both subscriptions and managing payment methods. You do not need database synchronization as you can load plan details directly from Stripe using the user's email. This is slower than a database call but typically can be executed in about 500ms.

Table of Contents

  1. Getting Started
    1. Quickstart via instant CLI
    2. Manual Installation
  2. Bootstrapping plans
    1. Plans: _instant/payments/plans.json
    2. Line Items: _instant/payments/line_items.json
      1. capacity Settings
      2. usage Settings
      3. flag Settings
    3. Bootstrapping via instant CLI
    4. Bootstrapping via npx payments bootstrap
  3. API Reference
    1. InstantPayments (class)
      1. InstantPayments.bootstrap
      2. InstantPayments.writeCache
    2. InstantPayments (instance)
      1. customers
        1. customers.find
        2. customers.subscribe
        3. customers.unsubscribe
      2. invoices
        1. invoices.list
        2. invoices.upcoming
      3. paymentMethods
        1. paymentMethods.list
        2. paymentMethods.create
        3. paymentMethods.remove
        4. paymentMethods.setDefault
      4. plans
        1. plans.list
        2. plans.current
        3. plans.billingStatus
      5. usageRecords
        1. usageRecords.create
  4. Deploying to different environments
    1. Deploying via instant CLI
    2. Deploying manually
  5. Acknowledgements

Getting Started

Quickstart via instant CLI

If you are using the instant command line tools, you can get started with Instant payments easily:

instant kit payments

This will perform the following actions:

  • Install @instant.dev/payments
  • Automatically set STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY in .env and .env.test
  • Create example _instant/payments/plans.json and _instant/payments/line_items.json for you
  • Create example endpoints for subscriptions, plans, paymentMethods and invoices
  • Create example tests for your endpoints

Once installed, run:

instant payments:sync
instant payments:sync --env test

To sync (bootstrap) your plans to Stripe and create a _instant/payments/cache/stripe_plans.json file. Then you're all good to go! You can run;

instant test

To verify everything worked correctly.

Manual Installation

To get started with Instant Payments, you'll first install the package locally:

cd ~/my/project/dir/
npm i @instant.dev/payments --save
npm i dotenv --save

Next, you should create a .env file in your root directory for usage locally. Populate it with keys found on dashboard.stripe.com/test/apikeys. Make sure you are on the right account.

File: .env

NODE_ENV=development
STRIPE_SECRET_KEY=[your-test-mode-secret-key]
STRIPE_PUBLISHABLE_KEY=[your-test-mode-publishable-key]

Next, we'll prepare a basic sample setup for our Stripe plans. We'll have two plans: Free and Basic. The Basic plan will cost $15.00 USD per month. Additionally, each plan comes with an AI Assistant. The default usage of the AI Assistant is capped at 5 messages per month, but the Basic plan unlocks 5,000 messages per month. We can do this by specifying settings overrides for specific line items.

To create the plans, we need to create two files, _instant/payments/plans.json and _instant/payments/line_items.json.

File: _instant/payments/plans.json

[
  {
    "name": "free_plan",
    "display_name": "Free",
    "enabled": true,
    "visible": true,
    "price": null,
    "line_items_settings": {}
  },
  {
    "name": "basic_plan",
    "display_name": "Basic",
    "enabled": true,
    "visible": true,
    "price": {
      "usd": 1500
    },
    "line_items_settings": {
      "ai_assistant": {
        "value": 5000,
        "display_value": "5,000 messages per month"
      }
    }
  }
]

File: _instant/payments/line_items.json

[
  {
    "name": "ai_assistant",
    "display_name": "AI Assistant",
    "description": "Number of messages you get with our helpful AI assistant",
    "type": "flag",
    "settings": {
      "value": 5,
      "display_value": "5 messages per month"
    }
  }
]

To get everything set up in Stripe we need to bootstrap our Stripe Products and Prices. We can do this easily with:

npx payments bootstrap development

And finally, to start developing with Instant Payments, in our code:

import InstantPayments from '@instant.dev/payments';

const payments = new InstantPayments(
  process.env.STRIPE_SECRET_KEY,       // we recommend loading these via dotenv
  process.env.STRIPE_PUBLISHABLE_KEY,  // from the same file as above
  `./_instant/payments/cache/stripe_plans.json` // created from npx payments bootstrap
);

// Return this as part of an API response
let subscription = await payments.customers.subscribe({
  email,
  planName: 'basic_plan',
  successURL: `https://example.com/success/`,
  cancelURL: `https://example.com/fail/`
});

Bootstrapping plans

Instant Payments automatically configures all of your Stripe Products and Prices for you based on two files, _instant/payments/plans.json and _instant/payments/line_items.json. These are files used to define your available subscription plans. This will then create a cache of your plans and associated stripe data in _instant/payments/cache/stripe_plans.json, which you will use to instantiate Instant Payments.

Plans: _instant/payments/plans.json

Plans represent basic subscription primitives that your customers can pay for. For example, a Free, Hobby and Pro tier. Your customers are charged monthly for these plans based on the "price" you set. For Instant Payments, you must have a free plan i.e. one plan with "price": null. Even if your product is unusable in a free state, a free plan must exist to represent a user without a subscription.

The basic structure of your plans.json looks something like this:

[
  {
    "name": "free_plan",
    "display_name": "Free",
    "enabled": true,
    "visible": true,
    "price": null,
    "line_items_settings": {}
  },
  {
    "name": "basic_plan",
    "display_name": "Basic",
    "enabled": true,
    "visible": true,
    "price": {
      "usd": 1500
    },
    "line_items_settings": {
      "ai_assistant": {
        "value": 5000,
        "display_value": "5,000 messages per month"
      }
    }
  }
]
  • name is the name of your plan for use in code, like customers.subscribe()
    • this will be used to uniquely identify your Stripe Product
  • display_name is the intended display name for customers
  • enabled determines whether or not users can subscribe to the plan
    • For free plans, if there are paid line items that can be added (type capacity or usage) then the customer will be unable to subscribe to these items if the value is false
    • this can be used for deprecating plans: if you don't want new users signing up on an old plan, you can just set "enabled": false
  • visible is purely cosmetic - it is intended to be used to change the visibility of a plan on a page rendered by the front-end
    • this can be used if you create a custom plan you don't want users to see, or if you want people to be able to see legacy plans ("enabled": false)
  • price is either null (free) or key-value pairs representing prices in specific currencies
    • values are in cents
    • currently only usd is supported, but we could add more soon!
  • line_item_settings allows you to override default line item settings for the plan
    • for example, if a plan has a different price for a line item (like seats, projects) or a different amount of free items included, set it here

Line Items: _instant/payments/line_items.json

Line Items represent additional Stripe Products your customers can pay for under the umbrella of a plan. There are three Line Item types: capacity, usage and flag.

  • capacity is for individually licensed items, like team seats
  • usage is for items that are charged for with metered billing
  • flag has no representation in Stripe; it's just a setting to indicate a max limit you can reference in your app

The basic structure of your line_items.json looks something like this;

[
  {
    "name": "collaborator_seats",
    "display_name": "Team seats",
    "description": "The number of team members that can actively collaborate on projects for this account.",
    "type": "capacity",
    "settings": {
      "price": {
        "usd": 2000
      },
      "included_count": 1
    }
  },
  {
    "name": "execution_time",
    "display_name": "Execution time",
    "description": "The amount of time your functions run for, measured in GB of RAM multiplied by number of seconds.",
    "type": "usage",
    "settings": {
      "price": {
        "usd": 500
      },
      "units": 1000,
      "unit_name": "GB-s",
      "free_units": 100
    }
  },
  {
    "name": "ai_agent",
    "display_name": "Pearl (AI Agent)",
    "description": "Amount of included usage of Pearl, your AI assistant.",
    "type": "flag",
    "settings": {
      "value": 5,
      "display_value": "5 messages per month"
    }
  }
]
  • name is the name used internally and by Stripe to reference the item, e.g. via customers.subscribe()
  • display_name is the customer-readable name of the item
  • description is a helpful customer-readable description of the item
  • type is the line item type: one of "capacity", "usage" or "flag"
  • settings are the default line item settings and can be overridden in plans.json for specific plans

capacity Settings

  • price is either null (free) or key-value pairs representing prices in specific currencies
    • values are in cents
    • currently only usd is supported, but we could add more soon!
  • included_count is the number included with the plan by default (for free)
    • if you want an unlimited included_count, use "price": null

usage Settings

  • price the price per units
    • it is an object of key-value pairs representing prices in specific currencies
    • values are in cents
    • currently only usd is supported, but we could add more soon!
  • units is the number of units represented by the price
    • in the example above, a price of 500 for "units": 1000 would mean $5.00 per 1,000 units
    • price has a minimum granularity of 1 cent, for smaller granularity increase your units
    • Instant Payments automatically handles all the math here, we recommend using human-readable settings here
  • unit_name is whatever your units are called, for example messages or errors or MB
  • free_units is the number of units included for free with the plan, use 0 to charge for all units

flag Settings

  • value is the value you reference in code for the flag
  • display_value is a customer-readable value for the flag

Bootstrapping via instant CLI

Bootstrapping via the instant command line tools is easy.

instant payments:sync # your dev environment
instant payments:sync --env test # all other environments

You'll want to run this before you deploy to staging or production.

Bootstrapping via npx payments bootstrap

To bootstrap you plans in Stripe manually, perform the following command where [env] is your environment. development would use your local environment.

npx payments bootstrap [env]

This will rely on STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY in your local .env file (if [env] is development), .env.staging (if [env] is staging), .env.production (if [env] is production) ... etc.

We recommend always running this command before deploying. If the correct prices / products exist it will not overwrite them, it just creates anything missing from your Stripe setup.

API Reference

To import Instant Payments using ESM;

import InstantPayments from '@instant.dev/payments';

And CommonJS:

const InstantPayments = require('@instant.dev/payments');

InstantPayments (class)

InstantPayments comes with two helper methods for manually bootstrapping and writing a stripe_plans.json cache.

InstantPayments.bootstrap

/**
 * Bootstraps Stripe with `metadata: instpay` products and prices corresponding to your line items
 * @param {string} secretKey         Your Stripe secret key
 * @param {string} plansPathname     Path to your plans.json object
 * @param {string} lineItemsPathname Path to your line_items.json object
 * @returns {object} bootstrapResult           The result of bootstrapping
 * @returns {object} bootstrapResult.cache     Your cached Stripe plans object
 * @returns {array}  bootstrapResult.Plans     Templates for your Plans from plans.json
 * @returns {array}  bootstrapResult.LineItems Templates for your Line Items from line_items.json
 */
await Instant.bootstrap(secretKey, plansPathname, lineItemsPathname);

InstantPayments.writeCache

/**
 * Writes a file to cache your bootstrapped plans and associated them with an environment
 * @param {string} cachePathname Desired pathname for your cached plans
 * @param {string} env           Desired environment (e.g. development, test, production)
 * @param {object} cache         JSON for your cached plans
 * @returns {boolean} success
 */
Instant.writeCache(cachePathname, env, cache);

InstantPayments (instance)

To create a new InstantPayments instance, use:

const payments = new InstantPayments(
  process.env.STRIPE_SECRET_KEY,
  process.env.STRIPE_PUBLISHABLE_KEY,
  `./_instant/payments/cache/stripe_plans.json` // created via bootstrapping
);

customers

Find, subscribe and unsubscribe customers.

customers.find
/**
 * Finds or creates a customer with provided email address
 * @param {string} email Customer email address
 * @returns {object} customer
 */
await payments.customers.find({email: '[email protected]'});
customers.subscribe
/**
 * Subscribes to a plan by creating a Stripe checkout session
 * @param {string} email Customer email address
 * @param {string} planName The name of the plan you wish to subscribe to
 * @param {object} lineItemCounts An object containing key-value pairs mapping line item names to *purchased* quantities, if left empty line items will be adjusted automatically to match the new plan
 * @param {object} existingLineItemCounts An object containing key-value pairs mapping to existing line item counts, if provided they are used to validate if within plan limits
 * @param {string} successURL URL to redirect to if the checkout is successful
 * @param {string} cancelURL URL to redirect to if the checkout is cancelled
 * @returns {object} subscription
 */
await payments.customers.subscribe({
  email: '[email protected]',
  planName: 'pro_plan',
  lineItemCounts: {seats: 4},                    // optional
  existingLineItemCounts: {seats: 2},            // optional
  successURL: 'https://my-website.com/pay/yes/', // Recommended: MUST exist if no payment method added
  cancelURL: 'https://my-website.com/pay/no/'    // Recommended: MUST exist if no payment method added
});
customers.unsubscribe
/**
 * Unsubscribes from active plan
 * @param {string} email Customer email address
 * @param {object} existingLineItemCounts An object containing key-value pairs mapping to existing line item counts, if provided they are used to validate if within plan limits
 * @returns {boolean} canceled
 */
await payments.customers.unsubscribe({
  email: '[email protected]',
  existingLineItemCounts: {seats: 2}, // optional
});

invoices

Lists invoices and finds the next upcoming invoice.

invoices.list
/**
 * Lists all invoices for a customer
 * @param {string} email Customer email address
 * @returns {array} invoices
 */
await payments.invoices.list({email: '[email protected]'});
invoices.upcoming
/**
 * Retrieves the upcoming invoice for the current user
 * @param {string} email Customer email address
 * @returns {?object} upcomingInvoice
 */
await payments.invoices.upcoming({email: '[email protected]'});

paymentMethods

List, create, remove, and set payment methods as default.

paymentMethods.list
/**
 * Lists all available payment methods for a customer
 * @param {string} email Customer email address
 * @returns {array} paymentMethods
 */
await payments.paymentMethods.list({email: '[email protected]'});
paymentMethods.create
/**
 * Creates a payment method using Stripe checkout
 * @param {string} email Customer email address
 * @param {string} successURL URL to redirect to if the payment method addition is successful
 * @param {string} cancelURL URL to redirect to if the payment method addition is cancelled
 * @returns {object} checkoutSession
 * @returns {string} checkoutSession.stripe_publishable_key         Key to use for creating Stripe checkout sessions
 * @returns {string} checkoutSession.stripe_checkout_session_id Checkout session id for use with Stripe's frontend library
 */
await payments.paymentMethods.create({
  email: '[email protected]',
  successURL: 'https://my-website.com/pay/card_added',
  cancelURL: 'https://my-website.com/pay/card_failed'
});
paymentMethods.remove
/**
 * Removes a payment method and sets a new default payment method if none set
 * @param {string} email Customer email address
 * @param {string} paymentMethodId The Stripe ID of the payment method to remove
 * @returns {array} paymentMethods List of all existing payment methods
 */
await payments.paymentMethods.remove({
  email: '[email protected]',
  paymentMethodId: 'card_xxx' // Stripe paymentMethod ID
})
paymentMethods.setDefault
/**
 * Changes a payment method to the default payment method for the customer
 * @param {string} email Customer email address
 * @param {string} paymentMethodId The Stripe ID of the payment method to remove
 * @returns {object} paymentMethod Payment method object created
 */
await payments.paymentMethods.setDefault({
  email: '[email protected]',
  paymentMethodId: 'card_xxx' // Stripe paymentMethod ID
})

plans

Lists all available plans, gets current plan for a customer, or finds the billing status (faster than getting the entire current plan).

plans.list
/**
 * Lists all available plans
 * @returns {array} plans Available plans
 */
await payments.plans.list();
plans.current
/**
 * Retrieves the plan a customer is currently subscribed to
 * @param {string} email Customer email address
 * @returns {object} planResult             Plan customer is currently subscribed to
 * @returns {object} planResult.currentPlan Current Subscription plan
 * @returns {array}  planResult.plans       All available plans
 */
await payments.plans.current({email: '[email protected]'});
plans.billingStatus
/**
 * Retrieves the billing status of the current customer plan
 * @param {string} email Customer email address
 * @returns {object}  planResult                           Plan customer is currently subscribed to
 * @returns {object}  planResult.currentPlan               Current Subscription plan billable summary
 * @returns {boolean} planResult.currentPlan.is_billable   is the plan billable?
 * @returns {boolean} planResult.currentPlan.is_incomplete is the subscription complete?
 * @returns {boolean} planResult.currentPlan.is_past_due   is the subscription past due?
 * @returns {string}  planResult.currentPlan.invoice_url   URL of the invoice, if incomplete or past due
 */
await payments.plans.billingStatus({email: '[email protected]'});

usageRecords

Creates new usage records for "type": "usage" line items. These will be billed at the end of each billing cycle.

usageRecords.create

NOTE: By default, this method can only be called per-customer once every 10 minutes. We recommend aggregating usage calls and sending them in a cron job.

/**
 * Creates a usage record for the customer
 * @param {string} email Customer email address
 * @param {string} lineItemName The name of the Line Item to record usage for
 * @param {integer{0,2147483647}} quantity The quantity to record
 * @param {integer{-12,0}} log10Scale Scale factor in which to adjust quantity x 10^n
 * @param {integer{-10,0}} log2Scale Scale factor in which to adjust quantity x 2^n
 * @returns {object} usageRecord
 */

To avoid floating-point arithmetic errors, we provide a log10Scale and log2Scale adjustment parameters. You should always pass an integer to quantity. If you need to use 0.1, send quantity: 1 and log10Scale: -1. We have included log2Scale up to -10, e.g. 1/1024 for fractions that are multiples of 1/2 and byte-related multiples (2^-10 = 1024).

await payments.usageRecords.create({
  email: '[email protected]',
  lineItemName: 'execution_time',
  quantity: 100,
  log10Scale: -3 // 1/1,000th of the quantity provided
  log2Scale: -10 // 1/1,024th of the quantity provided
})

Deploying to different environments

Instant Payments relies on .env files for bootstrapping Stripe in different environments. When deploying, tou need to make sure that STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY are set in any environment you deploy to.

Deploying via instant CLI

To deploy via the instant command line tools, make sure you first run:

instant payments:sync --env [deploy_target]

Where [deploy_target] the env you want to ship to. This will make sure Stripe for that environment is properly synced, and ensure STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY are set for those environments.

You can then deploy normally with:

instant deploy --env [deploy_target]

And that's it!

Deploying manually

We offer no specific guidance on manual deploys, but remember to bootstrap every time you deploy to ensure your deployment environment is in sync with Stripe.

Acknowledgements

Special thank you to Scott Gamble who helps run all of the front-of-house work for instant.dev 💜!

| Destination | Link | | ----------- | ---- | | Home | instant.dev | | GitHub | github.com/instant-dev | | Discord | discord.gg/puVYgA7ZMh | | X / instant.dev | x.com/instantdevs | | X / Keith Horwood | x.com/keithwhor | | X / Scott Gamble | x.com/threesided |