@instant.dev/payments
v0.0.7
Published
Payments library that uses Stripe as a system of record
Downloads
21
Readme
Instant Payments
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
- Getting Started
- Bootstrapping plans
- API Reference
- Deploying to different environments
- 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
andSTRIPE_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
andinvoices
- 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, likecustomers.subscribe()
- this will be used to uniquely identify your Stripe
Product
- this will be used to uniquely identify your Stripe
display_name
is the intended display name for customersenabled
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
orusage
) then the customer will be unable to subscribe to these items if the value isfalse
- 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
- For free plans, if there are paid line items that can be added (type
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
)
- 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 (
price
is eithernull
(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 seatsusage
is for items that are charged for with metered billingflag
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. viacustomers.subscribe()
display_name
is the customer-readable name of the itemdescription
is a helpful customer-readable description of the itemtype
is the line item type: one of"capacity"
,"usage"
or"flag"
settings
are the default line item settings and can be overridden inplans.json
for specific plans
capacity
Settings
price
is eithernull
(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
- if you want an unlimited
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 theprice
- 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
- in the example above, a price of
unit_name
is whatever your units are called, for examplemessages
orerrors
orMB
free_units
is the number of units included for free with the plan, use0
to charge for all units
flag
Settings
value
is the value you reference in code for the flagdisplay_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 |