Refund confirmation email: tell the customer what happens next

Updated

A refund email calms a worried customer. It should say that the money is on its way, how much, and roughly when, and it should never leave them wondering whether you did anything.

When it fires

On the payment provider's refund-created or refund-succeeded event, depending on how it reports state. Do not send it when a support agent clicks a button, because that action may still fail.

What to state

  • The refunded amount and currency
  • The original order or invoice reference
  • The payment method it returns to, at most the last four digits
  • What you control (issued today) and what you do not (the bank's processing time)
  • Whether the refund is full or partial

Implementation (Python)

import os

from inboxili import Inboxili, InboxiliError

client = Inboxili(os.environ["INBOXILI_API_KEY"])


def money(minor: int, currency: str) -> str:
    return f"{minor / 100:,.2f} {currency.upper()}"


def on_refund_event(db, event):
    refund = event["refund"]
    if not db.mark_refund_email_pending(refund["id"]):  # unique constraint: false if already handled
        return

    order = db.get_order(refund["order_id"])
    partial = refund["amount"] < order.total
    try:
        client.emails.send(
            to=order.customer_email,
            from_email="billing@yourdomain.com",
            from_name="Acme Billing",
            subject="Your refund of {{amount}} has been issued",
            html_body=(
                "<p>Hi {{name}}, we issued a {{kind}} refund of <strong>{{amount}}</strong> for order {{order}}.</p>"
                "<p>It returns to the original payment method. Your bank may take several business days to show it.</p>"
            ),
            text_body=f"Refund of {money(refund['amount'], order.currency)} issued for order {order.number}.",
            template_data={
                "name": order.customer_first_name,
                "kind": "partial" if partial else "full",
                "amount": money(refund["amount"], order.currency),
                "order": order.number,
            },
        )
        db.mark_refund_email_sent(refund["id"])
    except InboxiliError as e:
        db.mark_refund_email_failed(refund["id"], f"{e.status} {e.code}")

Marking the refund as pending before the call is what prevents a duplicate: if the provider redelivers the event, the second attempt finds the row and returns.

Things that go wrong

  • Duplicate emails from redelivered events. Use a unique constraint on the refund ID.
  • Wrong amount. Format from integer minor units, and use the refund's amount, not the order total.
  • Guessing the timeline. Avoid a specific number of days unless your provider documents it.
  • Ambiguous partial refunds. Say "partial" and what remains paid.
  • Sending on failure. If a refund fails, send nothing now and alert your team.

Delivery events

A bounced refund email is worth a human follow-up. The customer has no proof their money is coming.

Frequently asked questions

How long should I say a refund takes?
Say what you control: when you issued it. The time the money takes to appear depends on the customer's bank or card issuer, so give a range from your payment provider's documentation rather than a promise.
Should I send one email per partial refund?
Yes, each refund is a separate money movement. State the refunded amount and, if useful, the remaining paid amount.

Official SDKs

Send your first refund confirmation

Create a workspace, verify a domain, and make your first API call.

Related