Product block
A product card with a Buy now button that carries a real payment — no storefront, no separate checkout page to build, no card details ever touching MailInApp's own servers. Connect your own Stripe account under Settings → Payments first (Stripe Connect, Standard/OAuth); every charge settles directly into that account.
How it works
Unlike a vote or a rating, a purchase can only safely complete on Stripe's own hosted checkout page, so this block has no in-email kinetic tier at all. Buy now is link-first everywhere, mail included. Tapping it always opens the hosted live view first at a confirm page (/v/[token]/act?blockId=...&blockType=product&action=buy), which re-displays the product's name and price and asks for one more tap ("Continue to payment") before it actually creates anything.
That confirm-then-POST shape is deliberate: a security scanner or corporate proxy that prefetches every link in an email must never trigger a real Stripe Checkout Session on a bare GET. Only the explicit POST from the confirm page hits POST /api/checkout. That route resolves price, name and inventory from the block's own stored configuration server-side, never from anything the client sends. It then creates a Checkout Session on the owner's connected Stripe account, writes a pending order, and 303-redirects to Stripe.
Nothing about a pending order counts as a sale yet. Only POST /api/webhooks/stripe ever moves an order to paid. It's authenticated by Stripe's own signature, the same trust model as the countdown and open-tracking endpoints' HMAC URLs, since the success-URL redirect a buyer's browser lands on is UX only and can't be trusted as proof of payment.
On that webhook, MailInApp re-checks inventory transactionally before confirming. If two buyers were racing for the last unit and both got through Stripe, the loser's charge is automatically refunded and they're emailed a notice rather than the product silently being oversold. A confirmed purchase also writes a purchase interaction event, so revenue flows through the same aggregates and project webhook pipeline every other block's responses use. See Interactions & analytics.
Fulfillment depends on the Fulfillment field:
- Digital — you enter the delivery content (a download link or code) directly on the block; the moment the webhook confirms payment, that content is emailed to the buyer automatically.
- Physical — Stripe Checkout collects a shipping address as part of the payment flow; the order sits in the project's Orders table until you click Mark fulfilled, which emails the buyer a shipped notice.
Inventory is an optional cap (0 = unlimited). Mailed HTML is static at send time, the same convention the countdown block's deadline and the meeting-link block's join window follow, so a capped product's stock isn't baked in as text. Instead the email embeds a small server-rendered stock-pill image ("3 left" / "Sold out") that's regenerated on every open, so it never shows stale availability days after the send. On the hosted live view, inventory is checked live and dynamically: once paid orders reach the cap, Buy now is replaced outright with a disabled "Sold out" control rather than just a badge next to a still-clickable button. Either way, the webhook's transactional recheck at payment time is the actual source of truth; the badge and the disabled button are UX conveniences, not the enforcement mechanism.
If you've connected a Shopify or WooCommerce store, an Import from store search box appears above these fields, listing products from that store's own catalog. It stays in sync automatically in the background, with no separate button to press. Picking a product fills in Name, Description, Image and Price for you.
It's a one-time copy, not a live link. Every field stays yours to edit afterward, and the price actually charged at checkout is always whatever the block currently holds, never re-fetched from the store. The fields below work the same with or without a connected store — importing is a shortcut, not a requirement.
Configurable fields:
- Name, Description, Image — picked from your media library rather than typed as a URL, or filled in by Import from store above.
- Price and Currency (USD, EUR or GBP).
- Fulfillment — Digital or Physical, as above.
- Inventory — 0 for unlimited.
- Delivery content — digital-only; the download link or code sent after payment.
- Background color, padding, border, border radius, and text-style overrides for the name and description.
Examples
Subject
Back in stock: the Weekender Tote
A merchandise drop or a limited print run is the direct fit: cap Inventory at the run size and let the stock pill communicate scarcity as it depletes. See Ecommerce & product drops for the broader pattern of selling straight from a promotional send instead of routing clicks through a separate store. A digital course or ebook launch uses Digital fulfillment so the buyer gets the download link the instant payment clears, with no manual fulfillment step at all. A local business selling a physical add-on (a branded item, a seasonal box) uses Physical fulfillment and works the Orders table like a lightweight order queue, marking each one fulfilled as it ships. Use Contact buyer to open a support ticket if an address needs confirming.
What the static fallback looks like
<div style="margin:12px 0;padding:16px;border:1px solid #e5e7eb;
border-radius:8px;background-color:#ffffff">
<p style="margin:0 0 4px;font-weight:600;font-size:16px;color:#111827">
Limited edition tote bag
</p>
<p style="margin:0 0 10px;font-weight:700;font-size:18px">$29.00</p>
<img src="https://mailinapp.com/api/product-status?p=proj123&b=prod-1&s=..."
width="180" height="30" alt="" style="display:block;margin-bottom:8px" />
<a href="https://mailinapp.com/v/eyJhbGci.../act?blockId=prod-1&blockType=product&action=buy"
style="display:inline-block;padding:10px 20px;background-color:#4f46e5;
color:#ffffff;border-radius:6px;text-decoration:none;
font-size:14px;font-weight:600">
Buy now
</a>
</div>
The stock-status image only renders when an inventory cap is set; Buy now always opens the confirm page rather than posting a payment directly from inside the email.
Related
- Block reference overview
- Orders, fulfillment and refunds
- Support tickets
- Interactions & analytics
- Shopify / WooCommerce — connect a store to get the Import from store picker