brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
product-catalog-managementDodo Payments product catalog management, covering products with one_time_price, recurring_price, or usage_based_price, add-ons, product collections, product images, and digital file delivery. Use when creating or updating products, plans, or prices, building a storefront, uploading images, attaching downloadable files, or debugging a failed product update.
Independent GitHub snapshot · Automated classification · Original documentation governs
This skill covers the full product lifecycle: creating products with pricing models, managing add-ons and collections, uploading product images, and delivering digital files to customers.
Product model: Every product has exactly one pricing model selected at creation. The three models are:
one_time_price: charge once per purchase.recurring_price: charge on a configurable payment frequency and subscription period.usage_based_price: charge per unit consumed (see usage-based-billing skill for meter setup).Pricing is nested inside the product object. There is no separate top-level Price resource.
Pricing structure: Each price object contains:
type: one of the three models above.currency: ISO 4217 code (e.g., USD, AED, INR).price: amount in the smallest currency unit (cents for USD).discount: optional discount amount in the same unit.Tax category: Required at product creation. Dodo uses this to calculate and collect sales tax. Supported values are digital_products, saas, e_book, and edtech.
Lifecycle: Products support list, retrieve, update, and archive/unarchive. There is no delete endpoint. Archived products remain in your history but don't appear in new checkouts.
Images: Presigned upload URLs expire after 60 seconds. Upload to the URL immediately after requesting it.
Digital delivery: Entitlements grant customers access to files. Download URLs expire after roughly 15 minutes.
All products require a name, a pricing model, and a tax category.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
// One-time purchase
const product = await client.products.create({
name: 'Pro Bundle',
tax_category: 'digital_products',
price: {
type: 'one_time_price',
currency: 'USD',
price: 9900, // $99.00
discount: 0,
purchasing_power_parity: false,
},
});
console.log(product.product_id); // pdt_...
For recurring products, specify the billing cycle:
const subscription = await client.products.create({
name: 'Premium Plan',
tax_category: 'saas',
price: {
type: 'recurring_price',
currency: 'USD',
price: 2999, // $29.99/month
discount: 0,
payment_frequency_count: 1,
payment_frequency_interval: 'Month',
subscription_period_count: 1,
subscription_period_interval: 'Month',
purchasing_power_parity: false,
},
});
This skill is the canonical source for product creation request shapes. For a usage-based product, create the meter as described in the usage-based-billing skill, then attach it through the singular price.meters array:
const metered = await client.products.create({
name: 'API Calls',
tax_category: 'saas',
price: {
type: 'usage_based_price',
currency: 'USD',
discount: 0,
fixed_price: 0, // No fixed monthly charge; amounts use the smallest currency unit
payment_frequency_count: 1,
payment_frequency_interval: 'Month',
subscription_period_count: 1,
subscription_period_interval: 'Month',
purchasing_power_parity: false,
meters: [
{
meter_id: 'mtr_api_calls',
price_per_unit: '0.01',
free_threshold: 1000,
},
],
},
});
price_per_unit is a decimal string in the configured currency's smallest unit. The optional free_threshold excludes that many aggregated units before per-unit charging begins.
// List all products
const products = await client.products.list();
// Retrieve a single product
const product = await client.products.retrieve('pdt_pro_bundle');
PATCH /products/{id} (client.products.update) accepts the name, description, and metadata, and also price, tax_category, and pricing_mode. Every field is optional; omitted fields stay unchanged.
Two limits apply. The pricing model is immutable: you can edit the amount, but you cannot turn a one-time product into a subscription or usage-based one (or back) - create a new product instead. And a price edit only changes what new customers pay: existing subscriptions keep the price they were created with, so move them explicitly with Change Plan (client.subscriptions.changePlan(...)).
await client.products.update('pdt_pro_bundle', {
name: 'Pro Bundle (Updated)',
description: 'Includes all pro features',
metadata: {
category: 'software',
tier: 'premium',
},
});
Archive a product to hide it from new checkouts without deleting it:
await client.products.archive('pdt_pro_bundle');
// Restore it later
await client.products.unarchive('pdt_pro_bundle');
Upload a product image via presigned URL. The URL is valid for 60 seconds.
import fs from 'node:fs';
// Request a presigned upload URL
const uploadUrl = await client.products.images.update('pdt_pro_bundle', {
force_update: true,
});
// Upload immediately (within 60 seconds)
const response = await fetch(uploadUrl.url, {
method: 'PUT',
body: fs.readFileSync('./product.png'),
headers: { 'Content-Type': 'image/png' },
});
if (!response.ok) {
throw new Error('Image upload failed');
}
Add-ons are optional extras customers can purchase alongside a product. Create them independently, then reference them in checkout.
// Create an add-on
const addon = await client.addons.create({
name: 'Priority Support',
tax_category: 'saas',
currency: 'USD',
price: 4900, // $49.00
});
// Upload an image for the add-on
const addonImageUrl = await client.addons.updateImages(addon.id);
// List add-ons
const addons = await client.addons.list();
Group products into collections for themed checkouts. Collections have nested groups and items.
// Create a collection
const collection = await client.productCollections.create({
name: 'Starter Bundle',
description: 'Everything you need to get started',
groups: [
{
group_name: 'Core Tools',
products: [
{ product_id: 'pdt_tool_a' },
{ product_id: 'pdt_tool_b' },
],
},
],
});
// Add another group later
await client.productCollections.groups.create(collection.id, {
group_name: 'Optional Tools',
products: [
{ product_id: 'pdt_tool_c' },
{ product_id: 'pdt_tool_d' },
],
});
// Server route (e.g. POST /api/checkout): create the checkout session and return its URL
export async function POST() {
const session = await client.checkoutSessions.create({
product_collection_id: collection.id,
product_cart: [], // Required: pass an empty array for collection checkout
return_url: 'https://yoursite.com/return',
});
return Response.json({ checkout_url: session.checkout_url });
}
Never call the SDK or expose the API key in browser code. The browser only receives the URL from your server route and redirects:
// Browser
const res = await fetch('/api/checkout', { method: 'POST' });
const { checkout_url } = (await res.json()) as { checkout_url: string };
window.location.href = checkout_url;
Entitlements grant customers access to downloadable files. Create an entitlement, upload files, and generate download links.
// openAsBlob requires Node.js 20 or later
import { openAsBlob } from 'node:fs';
// Create an entitlement
const entitlement = await client.entitlements.create({
name: 'Pro Bundle Files',
integration_type: 'digital_files',
integration_config: {
digital_file_ids: [],
},
});
// Upload and attach a file as multipart/form-data (append `filename` before `file`)
const form = new FormData();
form.append('filename', 'pro-bundle.zip');
form.append('file', await openAsBlob('./pro-bundle.zip'), 'pro-bundle.zip');
const { file_id } = await client.entitlements.files.upload(entitlement.id, { body: form });
// Attach the entitlement to a product.
// `entitlements` REPLACES every entitlement already attached to the product:
// include all of them, send [] to remove all, or omit the field to leave them unchanged.
await client.products.update('pdt_pro_bundle', {
entitlements: [{ entitlement_id: entitlement.id }],
});
// A grant is produced by fulfillment when the customer purchases the product.
// Grants can be listed, revoked, or fulfilled with a manually managed license key;
// they cannot be created directly through the SDK.
// List grants for a customer
const grants = await client.customers.listEntitlementGrants('cus_customer_123');
// Generate a download URL (valid for roughly 15 minutes)
const grant = grants.items[0];
const downloadUrl = grant?.digital_product_delivery?.files[0]?.download_url;
Create short, shareable links to products:
const shortLink = await client.products.shortLinks.create('pdt_pro_bundle', {
slug: 'pro-bundle-deal',
});
console.log(shortLink.short_url); // https://dodo.link/pro-bundle-deal
For country or currency-specific pricing, use the localized-pricing skill. Do not hardcode prices in the client.
Expecting a delete endpoint: Products don't have a delete method. Archive them instead. Archived products remain in your history for reconciliation.
Trying to change the pricing model: You cannot switch a product between one-time, subscription, and usage-based pricing (for example one_time_price to recurring_price). Create a new product with the model you need.
Expecting a price edit to reprice existing subscribers: client.products.update(...) changes the amount for new customers only. Existing subscriptions renew at the price they were created with; move them with client.subscriptions.changePlan(...).
Hardcoding prices in the client: Always fetch the product catalog from the API. Prices change, and your client code will become stale. Read client.products.retrieve(id) to get the current price.
Forgetting the tax category: Every product requires a tax_category. Dodo uses this to calculate and remit sales tax. Omitting it will cause the create request to fail.
Uploading images after the 60-second window: Request the presigned URL and upload immediately. If the URL expires, request a new one.
Passing a non-empty product_cart for collection checkout: Collection checkout requires product_cart: []. The collection itself defines what's in the cart.
Confusing purchasing_power_parity with localized pricing: PPP is native. Set price.purchasing_power_parity: true to charge a business-wide, per-country percentage of the price (default false). It requires Adaptive Currency to be enabled, and a matching localized price always wins over PPP. See the localized-pricing skill.