Order confirmation email and abandoned cart emails for online stores
Published Updated 15 min readBy the Rasket team
Written for e-commerce teams and startup founders

The short answer
An order confirmation email is transactional: send it once, the moment payment succeeds, with the order number, every item, the total, the delivery address and what happens next. An abandoned cart email is marketing: send it only to shoppers who agreed to marketing, start around an hour after they leave, and stop the moment they buy.
Two failures are worth designing against. The confirmation goes out twice, or a shopper pays on their phone at 9:02 and at 10:00 their laptop gets “You left something behind”. Both are preventable, and the rules below work on any store platform.
No developer? Read the rules and skip the code
Shopify and most other hosted store platforms already send an order confirmation, and offer an abandoned checkout email you switch on as an automation. Keep the confirmation, and set up the reminder with the two abandoned cart sections below. The code samples are for a developer. On Shopify, the reminder’s recipients and wait are in the abandoned checkout automation, and the wait can be changed in Shopify Flow.
What an order confirmation email must contain
Customers open an order confirmation to check one thing: did it work? Answer that in the subject line, then give them what they will look for later.
- The order number, in the subject too: “Order 1042 is confirmed”. It is what they will quote when they write to you.
- Every item, with quantity, price and any variant, such as size or colour.
- The money: subtotal, shipping, tax, discounts and the total, plus the payment method in a safe form, such as “Visa ending 4242”.
- Where it is going: the shipping address, so they can spot a typo before it ships.
- What happens next: an estimated delivery date and a link to the order’s status page.
- Who you are and how to reach you: your business name and address, a reply address a person reads, and your returns policy.
In the UK this is also law for online sales to consumers. The Consumer Contracts Regulations say the trader “must give the consumer confirmation of the contract on a durable medium” within a reasonable time, and at the latest when the goods are delivered or before a service begins. It must include the information listed in the regulations’ Schedule 2, such as your identity, address, the total price and the right to cancel, unless you already gave it on a durable medium. An email counts. Booking and deposit confirmations follow the same logic: they confirm a purchase the customer made.
Shipping and delivery emails need less: the order number, the items in that parcel, the carrier, a tracking link and the expected date.
When to send it, and how to send it exactly once
Send it when the payment succeeds, not when the cart is submitted, and send it straight away. A confirmation that arrives ten minutes later produces a support email asking whether the order went through.
The common bug is sending it twice: a payment webhook is retried, a worker restarts, or two code paths both think they own the email. Two guards fix it. First, an Idempotency-Key header made from the order id. It is a label that says “this is the same email as before”, and Rasket answers a repeat within 24 hours with the first response instead of sending a second email (the idempotency reference lists every case). Second, the order record remembers that its confirmation went out, because a payment provider can keep retrying for longer: Stripe retries for up to three days.
// Call this when the payment succeeds, from your order code or a payment webhook.
async function sendOrderConfirmation(order, db) {
// Payment webhooks can be retried for days, so the order itself remembers.
if (order.confirmationEmailId) return order.confirmationEmailId;
// A mark with no id means an earlier attempt died mid-send: ask Rasket before sending again.
if (order.confirmationStartedAt) {
const found = await findConfirmation(order.number);
if (found) {
await db.orders.update(order.id, { confirmationEmailId: found });
return found;
}
}
await db.orders.update(order.id, { confirmationStartedAt: new Date() });
const rows = order.items
.map((item) => {
const variant = item.variant ? ` (${item.variant})` : "";
return `${item.quantity} × ${item.title}${variant}: ${item.total}`;
})
.join("\n");
const res = await fetch("https://api.rasket.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
"Content-Type": "application/json",
// A retry within 24 hours replays the first answer instead of sending again.
"Idempotency-Key": `order-${order.id}-confirmation`,
},
body: JSON.stringify({
from: "Acme Shop <orders@orders.acme.example>",
to: [order.email],
subject: `Order ${order.number} is confirmed`,
text: [
`Thanks, ${order.firstName}. We have your order ${order.number}.`,
"",
rows,
`Subtotal: ${order.subtotal} Discount: ${order.discount}`,
`Shipping: ${order.shipping} Tax: ${order.tax} Total: ${order.total}`,
`Paid with ${order.paymentLabel}. Shipping to ${order.address}.`,
`Arriving around ${order.deliveryEstimate}. Track it: https://acme.example/orders/${order.number}`,
"",
"Acme Shop, 1 Example Street, Springfield. Reply to this email with any question.",
"Returns within 30 days: https://acme.example/returns",
].join("\n"),
tags: [
{ name: "type", value: "order_confirmation" },
// Tag values take letters, digits, _ and - only.
{ name: "order", value: String(order.number) },
],
}),
});
// The caller retries a 429, a 5xx or a concurrent 409 later, with the same key.
if (!res.ok) throw new Error(`confirmation failed: ${res.status}`);
const { id } = await res.json();
await db.orders.update(order.id, { confirmationEmailId: id });
return id;
}
// The tag finds the send again while your plan keeps email records. This call reads emails,
// so it needs a full-access key: a sending-only key gets a 401.
async function findConfirmation(orderNumber) {
const res = await fetch(`https://api.rasket.com/emails?tags=order:${orderNumber}`, {
headers: { Authorization: `Bearer ${process.env.RASKET_API_KEY}` },
});
if (!res.ok) throw new Error(`lookup failed: ${res.status}`);
const { data } = await res.json();
return data[0]?.id ?? null;
}If two workers send at the same moment with the same key, the second gets 409 concurrent_idempotent_requests and no email goes out twice: retry that one later, like a 429 or a 5xx, with the same key and never a new one. A 409 invalid_idempotent_request is different: the body changed under a used key, and it fails every time, like a 400 or 422. Build the body from the stored order so a retry sends the same payload.
The sample also closes the last gap. If the process dies after Rasket accepts the email but before the order is updated, a payment retry after 24 hours would send a second one, because the key has expired. So the order is marked before the call, and a retry that finds the mark but no email id looks the email up by its order tag with GET /emails?tags=order:1042 before sending. That lookup reads emails, so it needs a full-access key, and it finds the email only while your plan keeps email records.
The sample lists every item, which matters: most orders have more than one. Rasket’s template library also has an order confirmation starter, but its item block holds one row, so for longer orders build the list in your own code as above. The stored id is where you start when a customer says the email never came.
On a hosted store with no code of your own, a duplicate usually means two systems send the same email, such as the platform’s confirmation and an app’s. Let one system own each email and switch the other off.
Keep the order confirmation transactional
An order confirmation stays transactional while it is about the order. A rule that works in practice: anything that would still make sense in the email if this order did not exist is marketing, so take it out. That covers a code for the next order, a product carousel and a newsletter sign-up box. Shipping, delivery and refund notices follow the same rule; a cart reminder or a win-back email is marketing.
The legal test behind that rule differs by country. In the US the FTC judges a mixed message by its primary purpose, while the UK’s ICO counts any marketing in a service message as direct marketing. So in the US a small offer after the order details can keep the email transactional; leaving it out is the choice that is safe everywhere. The transactional vs marketing post explains the primary-purpose test for mixed messages.
Send order mail from its own subdomain, a name under your domain such as orders.yourshop.com, and marketing, cart reminders included, from another, such as news.yourshop.com. Each takes a few DNS records, added once. This is for mail you send yourself; when your platform sends the order emails, its own sender settings decide. A subdomain separates the two streams only in part, as the subdomain guide explains.
Abandoned cart emails: who, when and how many
Most carts are left behind. The Baymard Institute’s average across 50 studies is a 70.22% cart abandonment rate (last updated September 2025). That counts shoppers who left, not how many a reminder brings back; the next section covers that.
Who may get one
A cart reminder exists to make a sale, so treat it as marketing, and send it only to shoppers who agreed to marketing. US law allows more, because CAN-SPAM works on opt-out, but UK and EU law are stricter. In the UK it takes consent or the “soft opt-in”, which covers details collected during a sale or negotiations for one, only for similar products, and only with a way to refuse at collection and in every email. The marketing email guide sets out the EU rules too. Every reminder needs an unsubscribe link. A store sending more than 5,000 messages a day to Gmail accounts must also support one-click unsubscribe, and Google asks it to keep reported spam below 0.1% and never reach 0.3%; the spam complaint rate guide shows how to watch it. Addresses typed at checkout carry typos: a hard bounce puts the address on Rasket’s suppression list, and Rasket stops mailing it.
Shopify’s help pages say an additional abandoned checkout automation sends only to customers subscribed to email marketing by default, while a migrated one keeps your old settings, so check yours. Rasket’s store flows enrol only subscribed contacts, no flow email reaches someone who opted out, and Rasket adds the unsubscribe headers for you.
When: about an hour, then stop
Shopify counts a checkout as abandoned once it has been incomplete for more than ten minutes. It gives an additional abandoned checkout automation a ten-hour default wait, which you can change in Shopify Flow. Ten hours means a shopper who left at lunch hears from you after dinner. No published study settles the best delay, so this is our starting point rather than a finding: about an hour, while the shopper still remembers the cart. Add a second reminder a day later only if it earns orders, stop at three, and test any change with the holdout below.
If you run a second reminder tool next to Shopify’s own, give Shopify’s a longer wait or send it only to shoppers who did not subscribe. Otherwise subscribers get both.
Cart or checkout?
An abandoned checkout email goes to someone who started checkout and gave their email address there. An abandoned cart email goes to a known subscriber who added items but never reached checkout, which you only see if your storefront reports cart events.
Never remind someone who already paid
Every reminder must stop the moment an order is placed, on any device. Use an exit event for the order, and check that the checkout is still open right before each send.
What should an abandoned cart email say?
It has one job: get the shopper back to their own checkout. Show the items they left, with their prices, and one button that opens that checkout. Add a line that invites questions by reply, because a doubt about sizing or delivery is often why they stopped. These are the three reminders we wrote for Rasket’s Shopify app, sent after one hour, one day and three days. Coming soon: Rasket is not in the Shopify App Store yet, so treat them as examples to copy.
| Reminder | Subject line | What it adds |
|---|---|---|
| After 1 hour | Ada, you left something at checkout | The items, a button back to the checkout, and an offer to answer questions by reply |
| After 1 day | Your checkout is still waiting | The same items, and a note that items can sell out |
| After 3 days | Last reminder about your checkout | Says it is the last one, so the shopper knows the emails stop |
None of the three carries a discount. A rule for discounts: leave them out of the first reminder, which is a nudge, because a code there gives margin away to people who were coming back anyway. If you add one, put it in the last reminder only, make the code single-use with an expiry, and watch whether returning customers start leaving checkouts on purpose. A discount that arrives every time teaches shoppers to wait for it.
Is it worth building?
No independent study publishes a recovery rate we can cite. The figures in circulation come from email vendors’ own customer data, measured in different ways. So measure your own, with a holdout: send no reminder to a random tenth of abandoned checkouts, and compare the share of each group that orders within three days. The difference is what the reminders earn; some of the shoppers who got one would have bought anyway. Without code, compare a month with the reminder on against a month with it off, knowing that seasons blur the result.
A worked example with made-up numbers: 300 abandoned checkouts a month from subscribers, an average order of $60, and reminders that lift the order rate by five points make 15 orders, or $900 a month. Compare that with the cost of building and checking the flow. On Rasket, 5,000 emails a month to 2,000 contacts costs $20 on the cheapest paid plan; the pricing page prices other volumes.
Build the abandoned checkout flow
Your store’s code reports checkout and order events to Rasket, which takes a developer. The automation itself can be built in the dashboard’s automation editor or with the JSON below. The steps are these:
- Report checkout starts — Send store:checkout.started with the shopper’s email, the checkout link and the items whenever a known shopper begins checkout.
- Report orders — Send store:order.placed with the order id when the order goes through; this is the event that ends every reminder for that shopper.
- Create the automation disabled — Trigger on the checkout event with the order as an exit event, wait an hour, check that the checkout is still open, send the first reminder, then repeat after a day.
- Read the email, then turn it on — Publish the reminder template, send yourself a test, and enable the automation only once the email and its checkout link read right.
Your code reports two events: when a known shopper starts checkout, and when an order is placed. Store events need no setup first, their payload has fixed keys, and money is sent as a string. The shopper must already be a subscribed contact, so the sample creates one first: an event for an address with no contact is kept but starts nothing, and an unsubscribed contact is never enrolled. Store events take their idempotency key in the body, as idempotency_key; POST /emails takes it as a header.
// Once, when a shopper ticks "Email me with news and offers":
POST https://api.rasket.com/contacts
Authorization: Bearer $RASKET_API_KEY
Content-Type: application/json
{ "email": "ada@example.com", "first_name": "Ada", "subscription_status": "subscribed" }
// When a shopper with an email address starts checkout:
POST https://api.rasket.com/events/send
Authorization: Bearer $RASKET_API_KEY
Content-Type: application/json
{
"event": "store:checkout.started",
"email": "ada@example.com",
"idempotency_key": "checkout-c_88",
"payload": {
"checkout_url": "https://acme.example/checkout/c_88",
"value": "240.00",
"currency": "USD",
"items": [{ "title": "Brass desk lamp", "quantity": 1, "price": "240.00" }]
}
}
// When the order is placed (this is what stops the reminders):
POST https://api.rasket.com/events/send
Authorization: Bearer $RASKET_API_KEY
Content-Type: application/json
{
"event": "store:order.placed",
"email": "ada@example.com",
"idempotency_key": "order-1042",
"payload": { "order_id": "1042", "value": "240.00", "currency": "USD" }
}Each store:checkout.started event saves the checkout on the contact as store.open_checkout, and the order clears it. So the condition step below asks a simple question before each reminder: is this checkout still open? The trigger’s exit_events also ends the run the moment the order arrives. A condition with no condition_not_met connection ends the run when the checkout is closed. Create the automation disabled, so nothing sends until you have read the emails:
POST https://api.rasket.com/automations
Authorization: Bearer $RASKET_API_KEY
Content-Type: application/json
{
"name": "Abandoned checkout",
"status": "disabled",
"steps": [
{ "key": "trigger", "type": "trigger",
"config": { "event_name": "store:checkout.started", "exit_events": ["store:order.placed"] } },
{ "key": "wait-1h", "type": "delay", "config": { "duration": "1 hour" } },
{ "key": "open-1", "type": "condition",
"config": { "type": "rule", "field": "store.open_checkout", "operator": "exists" } },
{ "key": "email-1", "type": "send_email",
"config": { "template": { "id": "<first reminder template id>",
"variables": { "checkout_url": "{{{event.checkout_url}}}" } } } },
{ "key": "wait-23h", "type": "delay", "config": { "duration": "23 hours" } },
{ "key": "open-2", "type": "condition",
"config": { "type": "rule", "field": "store.open_checkout", "operator": "exists" } },
{ "key": "email-2", "type": "send_email",
"config": { "template": { "id": "<second reminder template id>",
"variables": { "checkout_url": "{{{event.checkout_url}}}" } } } }
],
"connections": [
{ "from": "trigger", "to": "wait-1h", "type": "default" },
{ "from": "wait-1h", "to": "open-1", "type": "default" },
{ "from": "open-1", "to": "email-1", "type": "condition_met" },
{ "from": "email-1", "to": "wait-23h", "type": "default" },
{ "from": "wait-23h", "to": "open-2", "type": "default" },
{ "from": "open-2", "to": "email-2", "type": "condition_met" }
]
}{{{event.checkout_url}}} reads checkout_url from the event’s payload and hands it to the template’s checkout_url variable. The triple braces copy it as it is, so use them only for a link your own code builds. This sample stops at two reminders; the three reminders above add a third after two more days. The automations reference lists every step type, and the events reference every store event.
Not a shop? Receipts and trial emails in a SaaS product
A subscription product has the same split. A payment receipt and a failed-payment notice are about an agreement the customer already has, so they are transactional. Send them from your Stripe webhook handler with an idempotency key made from the invoice id and the email, such as invoice-in_123-receipt, because Stripe can deliver one event more than once. A failed-payment notice also needs the attempt number in its key, since one invoice can fail more than once.
| Stripe event | Type | |
|---|---|---|
| invoice.paid | Payment receipt | Transactional |
| invoice.payment_failed | Your payment failed: update your card | Transactional |
| customer.subscription.trial_will_end | Your trial ends on a date, and the card is charged then | Transactional while it states the terms; an upgrade pitch is marketing |
Stripe sends customer.subscription.trial_will_end three days before a trial ends, and immediately when a trial is cut short, so check the subscription before you send. The store: events are a reserved family for shops. For anything else, declare your own event once, such as trial.ending, with POST /events, then fire it with POST /events/send, and an automation that triggers on that name enrols the contact. The events reference has both calls. Flow runs are not billed, and the emails a flow sends count like any other email (automations).
Store emails in Rasket
Rasket sends both kinds of store email from one account and treats them differently: order mail goes out as transactional, with no unsubscribe header, while flow emails carry one-click unsubscribe headers and skip anyone who opted out.
When the Shopify app is listed, it will add ready-made abandoned checkout, abandoned cart, browse abandonment, welcome, post-purchase and win-back flows. It does not replace Shopify’s own order confirmation. Rasket is not the right tool if your platform’s built-in emails look right and arrive: keep them, and use Rasket for the flows and for order mail your own code sends. The Shopify guide has the details.
If a customer says an order email never arrived, tracing a missing message step by step shows where to look first.
Frequently asked questions
Is an order confirmation email transactional or marketing?
Transactional. The FTC’s CAN-SPAM guide lists confirming a transaction the recipient already agreed to as transactional. A subject line that reads like an ad, or a promotion before the order details, can make the FTC treat the whole message as commercial, so leave offers out.
Do I need consent to send abandoned cart emails in the US?
Not prior consent. CAN-SPAM works on opt-out, so a US cart reminder needs an honest subject, an unsubscribe link, your postal address, and a label saying it is an ad unless the shopper opted in. The UK and EU are stricter, so the rule that works everywhere is to remind only shoppers who agreed to marketing.
Does an abandoned checkout count for the UK soft opt-in?
Possibly. PECR’s soft opt-in covers details collected during a sale or negotiations for one, and a checkout the shopper started may count. It still applies only to similar products and needs a clear way to refuse when you collect the address and in every email. If you are unsure, ask with a consent box at checkout.
Do booking and appointment confirmations follow the same rules?
Yes. They confirm a purchase or an agreement the customer made, so they are transactional like an order confirmation: send them at once, keep promotions out, and include what was booked, when, the price and how to change or cancel it. Reminders before the appointment are transactional too.
What if the order confirmation bounces?
The address was probably mistyped at checkout. A hard bounce puts it on the suppression list, so a resend to the same address goes nowhere. Reach the customer another way, such as their phone number or the order page, correct the address on the order, and send the confirmation to the new one.
Are Stripe’s or Shopify’s own receipts enough?
Often, yes. Shopify sends its own order confirmation, and Stripe can email receipts for successful payments. Send your own when you need something their settings cannot do, such as your own layout, details they leave out, or one sending domain for every email you send.
Sources
- CAN-SPAM Act: A Compliance Guide for Business — Federal Trade Commission, read 2026-10-17
- The Consumer Contracts (Information, Cancellation and Additional Charges) Regulations 2013, regulation 16 — legislation.gov.uk, read 2026-10-17
- How do we identify direct marketing? — Information Commissioner’s Office, read 2026-10-17
- Recovering abandoned checkouts — Shopify Help Center, read 2026-10-17
- Opt in to the new abandoned checkout automation — Shopify Help Center, read 2026-10-17
- The Privacy and Electronic Communications (EC Directive) Regulations 2003, regulation 22 — legislation.gov.uk, read 2026-10-17
- Email sender guidelines — Google Workspace Admin Help, read 2026-10-17
- 15 U.S. Code § 7704: Other protections for users of commercial electronic mail — Legal Information Institute, Cornell Law School, read 2026-10-17
- Types of events — Stripe, read 2026-10-17
- Receive Stripe events in your webhook endpoint — Stripe, read 2026-10-17
- Cart abandonment rate statistics — Baymard Institute, read 2026-10-17
Related
- Shopify — Rasket for Shopify: customers with their consent, store events and ready-made flows.
- Templates — Fifty-seven starters sorted by goal, and how to keep a campaign as a template.
- Idempotency — Retry a send without sending it twice.
- Automations — Workflows that run per contact: start on a custom event or a segment change, wait, branch, and send. Every run pins a version and is inspectable.
- Marketing email: types, rules and examples — What marketing email is, how to tell it from transactional mail, types with shop and SaaS examples, and the US, EU, UK and Canadian rules, cited.
- Transactional vs marketing email: how to send each — How to tell transactional email from marketing email, the US and UK rules for mixed messages, separate sending subdomains, and how to send each kind.