Build a Cart Checkout Flow
A Cart allows you to combine any number of Plans from any Product onto a single Subscription: recurring Plans, add-ons, and one-off services like onboarding, bundled into one Stripe Checkout, meaning you can flexibly sell your whole Product suite in whatever combination each customer needs. The entire flow can be done in three API calls, without handling a single Stripe webhook.
Create the Cart
Set owner to the tenant the purchase belongs to, like a team or organisation ID, or the user ID if your application is single-user. For an anonymous customer who is yet to authenticate, use their session ID like session_abc123. If they sign up after their purchase, update the Owner's value to be the user's tenant via the update Owner endpoint PUT /api/owner/{id}.
| Parameter | Description |
|---|---|
owner | The tenant the purchase belongs to (see above). |
currency | Optional. Three-letter ISO 4217 code, case-insensitive. Omit to use geolocation (every Line Item on every Plan must share the same default currency). |
interval | day, week, month, year, or null for a one-off Cart. Must be set alongside intervalCount. |
intervalCount | The number of intervals between billing cycles, or null for a one-off Cart. |
import { Salable } from '@salable/sdk';
const salable = new Salable(process.env.SALABLE_SECRET_KEY);
const { data: cart } = await salable.api.carts.post({
owner: 'company_acme',
currency: 'USD',
interval: 'month',
intervalCount: 1
});Add a Plan to the Cart
Each Plan you add can be assigned to a Grantee, which is the ID of an entity in your system that will gain access through the Subscription, like a user or a Salable Group ID for a team. Once the customer completes checkout and the Subscription is active, every Entitlement attached to that Plan is granted to the Grantee; to check this in your code, perform an entitlement check.
Note Only the Line Items on the Plan that match the Cart's
currencywill be included at checkout.
| Parameter | Description |
|---|---|
cartId | The ID of the Cart to add this item to. |
planId | The ID of the Plan being purchased. |
interval | day, week, month, year, or null for one-off Plans. |
intervalCount | Must match the Cart's intervalCount, or null for one-off Plans. |
grantee | Optional. A Grantee's ID for one user, or if the Plan has a per-seat Line Item, a Salable Group ID (prefixed grp_) to onboard a team. Cannot be an email address. |
metadata | Optional. To override default quantities set the keys of the object to the Line Item slugs with a value of { quantity: N }. Can't be set for metered Line Items or per-seat Line Items with a fixed billing scheme. |
import { Salable } from '@salable/sdk';
const salable = new Salable(process.env.SALABLE_SECRET_KEY);
const { data: cartItem } = await salable.api.cartItems.post({
cartId: cart.id,
planId: 'plan_01HXYZ...',
interval: 'month',
intervalCount: 1,
grantee: 'user_alice'
});If not provided in the metadata, a Line Item's quantity will default to its minimum. However, if the grantee is set to a Salable Group ID (prefixed with grp_) the per-seat Line Item will instead default to the larger of the Group's member count or the minimum quantity. A Group's member count cannot exceed a Line Item's maximum quantity.
Pass metadata to override defaults per Line Item, for example, explicitly setting ten seats on a per-seat Line Item or eight hours of consultancy on a flat-rate Line Item.
One-off Line Items are included automatically as part of Plans that also include recurring Line Items, so a monthly subscription bundled with a one-off setup fee is bought as one Plan at the interval you send. Only a Plan whose Line Items are all one-off is treated as one-off, that's when you send interval and intervalCount as null.
Note If the Cart's
intervalandintervalCountarenull, and it has no Cart Items, the Cart is considered empty. The first Cart Item that is added with anintervalandintervalCountupdates the Cart to match. Subsequent Cart Items must have at least one Line Item that matches the Cart'scurrency. Cart Items with recurring Line Items must match the Cart'sintervalandintervalCount.
Generate the Checkout URL
Call POST /api/carts/{cartId}/checkout with the parameters below. The response contains a data.url you can redirect the customer to.
| Parameter | Description |
|---|---|
successUrl | Optional. Where Stripe redirects after successful payment. Falls back to the url in the Plan's Product settings if set, otherwise it will use a Salable hosted page. |
cancelUrl | Optional. Where the customer returns if they abandon checkout. Falls back to the url in the Plan's Product settings if set, otherwise it will use a Salable hosted page. |
email | Optional. Pre-fills the customer email in the checkout form if the Owner's associated Stripe Customer does not already have an email set. |
import { Salable } from '@salable/sdk';
const salable = new Salable(process.env.SALABLE_SECRET_KEY);
const { data } = await salable.api.carts.byId(cart.id).checkout.post({
successUrl: 'https://yourapp.com/welcome',
cancelUrl: 'https://yourapp.com/pricing',
email: 'customer@example.com'
});
return data.url; // https://checkout.stripe.com/c/pay/cs_live_...Note Any property you leave unset falls back to the value set in the Product settings of the Plans on the Cart. If two Products have setting values that conflict, the request is rejected and you will need to provide them explicitly.
After the customer pays, the Cart status moves from active to complete. Salable then creates the Subscription from the Cart's recurring Line Items and generates a Receipt for any one-off Line Items. If the customer leaves your application without purchasing, their Cart stays active until you call DELETE /api/carts/{id} to move it to abandoned.
Salable emits the subscription.created event after creating the Subscription, and receipt.created alongside it if the Cart held any one-off Line Items.
Related
- Cart and Checkout The full Cart lifecycle: Groups, the anonymous-owner flow, and the complete status model.
- Check Entitlements Gate features on the Entitlements the Subscription grants once checkout completes.