Part 2 of 7 · Refund handler series ~4 min read

How a refund request arrives

The handler can only help with requests it actually sees. So the first job is catching every refund request, no matter how it came in. There are three ways one gets into the queue: a customer emails (or your team forwards an email) to a help address, someone fills in the refund form on your site, or a rep pastes in a request that arrived by phone or live chat. The first two are automatic. The third exists because in real life plenty of refund asks never start as an email at all.

Key takeaways

  • Three intake lanes feed one queue: a help inbox, a contact-form webhook, and a manual paste lane.
  • Inbound emails arrive via SES; a small reader pulls out who, what, order, and amount.
  • The contact form posts straight to a Lambda Function URL — no API Gateway.
  • The paste lane lets a rep drop a phone or chat request into the same flow.
  • All three land as one clean request record on a single SQS queue for the checker.

Three lanes into one queue

Three intake lanes funnel into one queue A diagram with three vertical lane columns at the top and a single unified row at the bottom. Lane one, Help inbox: a customer emails or a teammate forwards an email to a dedicated help address; SES writes the raw message to S3; a reader Lambda pulls out the customer, the item, the order reference, and the amount, then drops a clean request record on the queue. Lane two, Contact form: the refund form on the website posts straight to a Lambda Function URL; the same reader normalizes the fields into a request record. Lane three, Manual paste: a rep pastes a phone or chat request into a small internal form that also posts to a Function URL; the reader treats it exactly like the others. All three lanes converge on one SQS queue, which holds each request as a clean record until the checker reads it. A note at the bottom: every lane produces the same record shape, so the checker never has to care how a request arrived. Lane 1 · email Help inbox • Customer emails or team forwards • SES writes message to S3 • Reader pulls who, what, order, amount • Clean record onto the queue Lane 2 · Function URL Contact form • Refund form on your site • Posts to a Lambda Function URL • Reader normalizes the fields • Same record onto the queue Lane 3 · rep paste Manual paste • Phone or chat request • Rep pastes into a small internal form • Same reader, same record shape • Onto the same queue One SQS queue (clean request records) customer · item · order ref · amount · asked-for · source · received-at every lane produces the same shape — checker reads from here to checker, per request Every lane produces the same record shape — the checker never has to care how a request arrived.
Fig 2. Three lanes converge on one queue. Email, web form, and rep paste all produce the same clean request record. The reader does the work of turning each into the same shape, so the checker downstream only ever sees one kind of thing.

Lane 1: the help inbox

The most common lane. Set up a dedicated inbound address — something like refunds@your-company.com — via Amazon SES. Customers email it directly, or your team forwards the original refund email to it. SES writes the raw message to s3://rf-raw-mime/. The S3 write triggers a reader Lambda that walks the message, finds the customer’s text (and any forwarded original underneath it), and calls Bedrock Haiku 4.5 — a small, fast, cheap model — to pull out the parts that matter: the customer’s name and email, the item, the order or receipt reference if it’s mentioned, the amount, and what they’re actually asking for (full refund, partial, replacement).

The model prompt is short and strict: “Read this refund request. Return JSON only with these fields. If a field isn’t in the text, leave it blank — do not guess an order number or an amount.” A blank field is fine; a made-up order number is not, because it would send the wrong refund down the wrong path. The cleaned record goes onto the SQS queue, and the checker picks it up from there.

Lane 2: the contact form

Plenty of businesses already have a “request a refund” form on their site. Lane 2 wires that form straight into the handler. The form posts to a Lambda Function URL — a plain HTTPS endpoint on a single Lambda, with no API Gateway in front of it (which keeps the cost at zero when the form is quiet). The form already has structured fields — name, email, order number, reason — so the reader has less to pull out; it just normalizes them into the same record shape and drops it on the queue.

The Function URL checks a shared secret the form includes, and a small rate limit keeps a bot from flooding the queue. Because the fields are already separate, this is the cleanest lane — the request record is almost complete before any model touches it.

Lane 3: manual paste

Some refund asks never arrive as text you control. The customer called. They mentioned it in a live chat that closed. They told a rep in the shop. Forcing those into “please email us so the system sees it” is a fight you don’t need — the rep is right there and can just write it down.

Lane 3 is a small internal form: the rep pastes or types what the customer said, adds the order number if they have it, and submits. It posts to the same Function URL as Lane 2 with a flag marking the source as “paste,” and the same reader turns it into the same record. Marking the source matters later — the audit trail shows a pasted request came from a person, not from a verified email address, so the approver knows to double-check the order reference.

Why everything funnels to one queue

Three lanes in, but only one queue the checker reads from. That’s on purpose. If each lane had its own path through the system, every “why did this refund get approved?” question would mean checking three different flows. Funneling everything into one queue with one record shape means there is exactly one path a request can take, and the checker, the drafter, and the audit log all see the same thing. The queue also gives you a safety net: if the checker has a bad moment, requests wait safely in the queue (and a dead-letter queue catches anything that fails repeatedly) instead of getting lost.

Next post: how the checker reads a request, pulls the exact policy lines that apply, and picks one of four outcomes — grounded only in what your policy actually says.

All posts