CommerceForAI

Discounts, buy links & reports

The everyday selling tools. All of them work from the dashboard, the SDK and the MCP server.

Discount codes

Create a code and it works at checkout straight away. Shoppers type it on Stripe's checkout page, and Stripe enforces the minimum order, usage limit and end date.

await commerce.discounts.create({ code: "SUMMER20", kind: "percent", value: 20, ends_at: "2026-09-01" });
await commerce.discounts.create({ code: "FIVEOFF", kind: "amount", value: 500, min_subtotal: 3000, max_redemptions: 100 });

// Apply one up front, e.g. for a link in an email campaign:
await commerce.checkouts.create({ items, success_url, discount_code: "SUMMER20" });

// Pause a code without deleting it:
await commerce.discounts.update("SUMMER20", { active: false });

Orders record the amount taken off in discount and the code in discount_code. Codes are mirrored into your own Stripe account, so they also show up in Stripe.

Customer accounts

Customers don't need a password. Link "My orders" on your site to your store's account page (customer_account_url on the store). They enter their email and get a private link to their orders, tracking, subscriptions and a button to request a return. The same link is in every order confirmation email, so there's no login to build.

const { data: store } = await commerce.store.get();
// <a href={store.customer_account_url}>My orders</a>

// Customers can ask for a return up to 30 days after delivery (the default). You approve or reject each one.
await commerce.store.update({ returns_window_days: 45 });

Editing an order

Before an order ships you can swap an item (a different size), remove or add items, or fix the address. The customer is refunded the difference and gets one email with what changed.

const { data: preview } = await commerce.orders.edit(order.id, {
  replace: [{ variant_id: "var_medium", with_variant_id: "var_large" }], dry_run: true });
// preview.refund, preview.fulfillment (print-on-demand orders that will be cancelled and sent again)
await commerce.orders.edit(order.id, { replace: [{ variant_id: "var_medium", with_variant_id: "var_large" }] });

Subscriptions

Give a variant a schedule and it's sold as a subscription. Every renewal Stripe charges becomes a new order, so stock, emails, print-on-demand and shipping work exactly as they do for one-time orders.

await commerce.products.create({ name: "Coffee of the month", variants: [
  { name: "Monthly", price: 1800, recurring: { interval: "month" } },
  { name: "Every 2 weeks", price: 1800, recurring: { interval: "week", interval_count: 2 } },
] });

// Each renewal: order.paid, with metadata.subscription_id
await commerce.subscriptions.list({ status: "active" });
await commerce.subscriptions.cancel("sub_...");             // at the end of the paid period
await commerce.subscriptions.simulateRenewal("sub_...");    // test stores: run a renewal now
  • All subscription items in one checkout must renew on the same schedule. One-time items in the same cart are charged once.
  • Stripe's subscription checkout has no shipping choice, so your first checkout shipping rate is added as a "Shipping" line charged every period.
  • Customers manage their own: every subscription order's confirmation email links to a page where they can cancel (at the end of the period they've paid for) or undo that. You get an email when someone cancels.

Gift cards

A gift card is a code with a balance. Customers enter it in the code box on the payment page (or you pass it as discount_code), and whatever they don't spend stays on the card for next time.

// Store credit or a prize, emailed to the recipient:
await commerce.giftCards.create({ value: 5000, recipient_email: "sam@example.com", send: true, message: "Sorry for the delay!" });

// Sell them: one card per unit bought, emailed to the line's recipient_email (or the buyer)
await commerce.products.create({ name: "Gift card", metadata: { gift_card: "true" },
  variants: [{ name: "$25", price: 2500 }, { name: "$50", price: 5000 }] });

Stripe's checkout has no gift-card payment method, so a card is applied as a one-time code worth its balance on your Stripe account. That means it comes off the products (not shipping), it doesn't combine with another discount, and with sales tax on, tax is worked out on the reduced amount. Ask your accountant if that matters where you sell.

Draft orders and invoices

Build an order for a customer yourself (a phone order, a wholesale buyer, a quote), at your own prices if you like, and email them an invoice with a Pay now link. The link works for 30 days. When they pay, it becomes a normal order: stock, emails and fulfillment work as usual. No stock is held until they pay.

const { data: draft } = await commerce.draftOrders.create({
  items: [{ variant_id: "var_...", quantity: 24, unit_price: 1800 }], // wholesale price, in cents
  customer_email: "buyer@shop.example", customer_name: "Pat",
  shipping: { name: "Freight", amount: 2500 },
  note: "Net 30 as discussed.",
});
await commerce.draftOrders.sendInvoice(draft.id, { message: "Here's the invoice for your order." });
// Or share draft.invoice_url yourself. When it's paid: draft_order.completed, with order_id.

Automatic discounts

No code needed: every checkout that qualifies gets it. If several apply, the one that takes off the most wins. Only one discount applies per checkout, so a code passed with discount_code replaces the automatic one.

// 10% off orders of $50 or more, for everyone, until the end of the month
await commerce.discounts.create({ automatic: true, kind: "percent", value: 10, min_subtotal: 5000, ends_at: "2026-12-01" });

Abandoned-checkout recovery

On by default. When a checkout expires without payment, Stripe creates a link back to the same cart, and the shopper gets one email from your store with that link. You also get a checkout.abandoned event containing email and recovery_url, if you'd rather send your own.

US shoppers can opt out on the checkout page. Outside the US, the email only goes to shoppers who opt in. It needs customer emails on (they are by default). Turn it off with store.update({ recovery_emails: false }).

Image hosting

Product photos, logos and print files can live on our CDN: no S3 or Cloudinary to set up. Uploads are PNG, JPEG or WebP up to 25 MB (5 GB per store, included). Photo metadata such as location is removed, and URLs never change.

import { readFileSync } from "node:fs";

const { data: art } = await commerce.files.upload(readFileSync("art/half-dome.png"), { filename: "half-dome.png" });
await commerce.products.create({ name: "Half Dome Tee", images: [art.url], print_files: [{ placement: "front", url: art.url }], price: 3600 });

// Products whose images live on another site: copy them over (repeat until remaining is 0)
await commerce.files.copyAllProductImages();

Branded emails & team alerts

Customer emails (order confirmed, shipped, refunded, return approved, cart reminder) use your logo, button color and footer address. Preview each one on the store's Emails tab, or send all of them to yourself.

await commerce.store.update({
  email_brand: { logo_url: "https://yoursite.com/logo.png", accent_color: "#2f6f4e", address: "1 Main St, Springdale UT" },
  owner_alerts: { new_order: true, low_stock_threshold: 3 }, // null turns low-stock emails off
});
await commerce.emails.sendTest({ template: "all" }); // emailed to the store owner

Your team gets an email for every new order, and once when a product option's stock falls to the threshold (restocking resets it).

Buy links (no website needed)

Every store has a public page listing its products, and every product has its own page with a Buy button that opens Stripe checkout. Share them on Instagram, in an email or as a QR code.

const { data: store } = await commerce.store.get();
store.hosted_store_url; // https://commerceforai.com/s/st_...
// Product pages: <hosted_store_url>/<product metadata.handle or id>

Only active products appear. Stock shows only as in stock or sold out. Turn the pages off with store.update({ hosted_pages: false }).

Collections

Group products for navigation. Storefronts filter by collection, and the hosted store page shows each collection as a section.

await commerce.collections.create({ name: "Mugs", product_ids: [mugA.id, mugB.id] }); // handle: "mugs"
const { data } = await commerce.products.list({ collection: "mugs" });

Customers

Everyone who has paid, matched by email, with their order count, total spent (after refunds), last shipping address and order history. You don't need to keep a separate customer record in sync.

const { data } = await commerce.customers.list({ sort: "spent" });
const { data: rae } = await commerce.customers.get("rae@example.com");

Sales reports

const { data } = await commerce.reports.sales({ days: 30 });
// data.totals: { orders, gross, net, refunds, discounts, average_order, customers, returning_customers }
// data.previous.change_pct, data.series (by day/week/month), data.top_products

Reports count paid, shipped and refunded orders. Simulated orders are left out of reports and customers.