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, which resolves price, name and inventory from the block's own stored configuration server-side — never from anything the client sends — 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 — authenticated by Stripe's own signature, the same trust model as the countdown and open-tracking endpoints' HMAC URLs — ever moves an order to paid, 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). Because mailed HTML is static at send time — the same convention the countdown block's deadline and the meeting-link block's join window follow — 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.
Configurable fields:
- Name, Description, Image — picked from your media library rather than typed as a URL.
- 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 and using 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.