# Win back the customers who are already returning

**The outcome:** a short list of former customers who are on your site right
now, and a reason to contact each one this week rather than next quarter.

**Who this is for:** anyone sitting on a lapsed-customer list they have not
emailed because they know a blast would burn it.

**Time:** an hour to build, five minutes a week to run.

---

## Before you start

- You need an **API key with BOTH scopes** — `read` for the visitor list in
  Step 2, `copilot` for the questions in Steps 1, 3 and 6. Scopes are chosen at
  creation and fixed, so ask for both up front rather than minting twice.
- You need the **pixel live**.
- You need a **connected store**, because the lapsed half of this comes from
  your orders.
- Useful but not required: a **connected email platform**, if you want the list
  pushed rather than read.

---

## Your store already knows who lapsed. That is not the hard part.

Every commerce platform can give you "customers who have not ordered in 90
days". Most teams have that list. Almost nobody emails it, and the ones who do
usually regret it, because a lapsed list is mostly people who lapsed for a
reason and a blast to all of them is how you teach a mailbox provider that your
domain sends mail nobody wants.

The question that changes the economics is not *who lapsed*. It is **which of
them is looking again right now**. That one is invisible in your store, because
a former customer browsing anonymously leaves no order and no session you can
tie to their name.

That is the join this playbook is about: your order history knows who they
were, Signal knows who is on the site today, and the overlap is the only part
worth acting on.

## The numbers, measured, so you size this correctly

Run on a real workspace on 2026-09-17:

| | |
|---|---|
| Lapsed customers (no order in 90 days) | **8,926** |
| Identified visitors on the site | 67,494 |
| Lapsed customers ever identified on the site | **166** |
| …of those, seen in the last 30 days | **33** |

Look at the last row before you decide this is too small to bother with.

**33 is the number you can act on well.** Eight thousand is a number you can
only act on badly. The entire value here is that the list is short enough to
write a real sentence to each person, and current enough that the sentence is
about something they did this week.

And be clear about why the overlap is 166 and not 8,926: identity resolution
catches a fraction of traffic, not all of it. Most lapsed customers who visit
are never identified. That is the honest ceiling of this play, it is not going
to improve much by tuning, and it is still the best-timed list you will get.

## Step 1 — Establish the lapsed set, on your terms

"Lapsed" is a business definition, not a technical one, and the right window
depends on your repurchase cycle. A coffee subscription lapses in six weeks; a
mattress does not lapse in two years.

```json
{"question": "How many distinct customers bought in the last 90 days versus the 90 days before that, and what is the repeat purchase rate?"}
```

Use the repeat rate to pick the window rather than defaulting to 90 days
because it is round. If most repeat orders land within 40 days, then 90 days is
already deep into "gone" and you are being too patient.

## Step 2 — Find which of them are back

**This step is a join you run, not a question you ask.** Signal has no tool that
returns it, and that is not an oversight — only about 0.5% of identified
visitors carry order data (350 of 67,656 on the workspace measured above), so
an endpoint that tried to do this join inside Signal would return almost
nothing and look broken. The two halves live in two systems and you own the
join.

**Half one, from your store or ESP:** customer email and last order date.
Every commerce platform exports this; it is the lapsed list you already have.

**Half two, from Signal:** who has been identified, and when they last visited.

```bash
curl -s "https://app.signal.geysera.com/signal-api/v1/visitors?page_size=200" \
  -H "Authorization: Bearer $SIGNAL_API_KEY"
```

Page through it — `total`, `page` and `page_size` come back in the envelope —
and keep `resolved_email` and `last_visit_at`.

**The join**, in whatever you already use:

```python
lapsed = {          # from your store: email -> last order date
    r["email"].lower(): r["last_order_at"]
    for r in store_export
    if r["last_order_at"] < ninety_days_ago
}

back = [
    v for v in signal_visitors
    if v.get("resolved_email")
    and v["resolved_email"].lower() in lapsed
    and v["last_visit_at"] > thirty_days_ago
]
```

`back` is the list. Expect it to be short — that is the point of the table
above, not a sign you did it wrong.

Two things to get right. Lowercase both sides before comparing, or you will
lose matches to capitalisation that means nothing. And do not widen the window
to make the number bigger: a longer window buys you people who are not actually
back, which is the whole thing this play is avoiding.

## Step 3 — Read what they looked at, one at a time

This is the step people skip, and it is where the return on the whole exercise
sits.

```json
{"question": "What pages has dana@acme.com viewed, in order, and when?"}
```

A former customer on your pricing page is a different email from a former
customer reading a support article. One is reconsidering, the other has a
problem with the thing they already bought. Sending the reconsidering email to
the second person is worse than sending nothing.

Thirty-three people is few enough to do this individually. That is the argument
for the short list restated: it makes a five-minute-per-person step affordable.

## Step 4 — Write to the behaviour, not to the segment

The email that works here is short and specific and does not mention that they
lapsed. "We noticed you have not ordered in a while" tells someone you have
been watching and have nothing to say.

What you have instead is a reason to write today: they looked at something.
Reference the category, not the page view — "you were looking at the refill
packs" is helpful; "you visited /products/refill-6pack at 14:32" is unsettling
and will cost you more than the order is worth.

**Draft it in your own model, not in Signal.** Signal answers questions about
your data; it has no drafting tool, and asking it to write copy is refused
rather than attempted — which is the right boundary, but it means the drafting
step belongs on your side.

The pattern that works: take the visitor's page history from Step 3, paste it
into whatever model you already use, and ask for the email there. The
[LLM tools workflow](../workflows/give-signal-to-any-llm-as-tools.md) wires
both halves into one loop, with Signal as the reader and your model as the
writer.

Then read it before it goes. Every time.

## Step 5 — Decide what you are actually offering

A discount is the reflex and usually the wrong first move. You are writing to
someone who already bought at full price once; leading with money teaches them
to wait for the next discount, and trains your best segment into your worst.

Better first moves, in rough order of how often they work:

- **Answer the question they were researching.** They were on a comparison
  page for a reason.
- **Tell them what changed** since they last bought, if anything did.
- **Make reordering trivial** — the friction is often the whole reason.
- **Then**, if none of that lands, consider an offer.

## Step 6 — Measure it as a cohort, not as a campaign

Open rates will look excellent, because the list is tiny and well-targeted.
That number will teach you nothing.

The question is whether these people ordered:

```json
{"question": "How many distinct customers bought in the last 30 days, and how many of those were returning rather than new?"}
```

Run it before you start and again a month later, and look at the returning
count. If you want to know whether the campaign caused the change rather than
coincided with it, that is a different question and there is exactly one tool
for it — see [Prove a change actually
worked](../workflows/prove-a-change-actually-worked.md).

## Run it weekly, not once

The list regenerates. Someone who lapsed and came back this week was not on
last week's list, and the reason this works is timing — a week late is most of
the value gone.

This is a good standing job for an agent: once a week, pull the overlap, drop
anyone already contacted, and put the rest in front of a human with what they
read. See [Give Signal to any LLM as
tools](../workflows/give-signal-to-any-llm-as-tools.md) for the loop, and keep
the send manual until you have watched it be right several times.

## What will go wrong

**The list will be smaller than you hoped.** That is identity resolution, not a
configuration error. Thirty-three well-timed emails will out-earn eight thousand
untimed ones and it will still feel wrong.

**Someone will want to email the whole lapsed list anyway.** The measured
overlap is the argument against it: you have evidence that 0.4% of them are
demonstrably back, and no evidence about the rest.

**A consumer-mailbox workspace will see `personal` dominate.** Consumer
addresses roll up under that sentinel, so filter it out of anything you are
treating as company-level, and do not read it as one enormous account.

**Someone will reply "how did you know I was looking?"** Have an answer ready
that you are comfortable saying out loud. If you are not comfortable saying it,
that is worth knowing before you send, not after.

---

## Next

- [Prove a change actually worked](../workflows/prove-a-change-actually-worked.md)
  — whether the campaign caused the lift or coincided with it.
- [Write a first-touch email from what they read](../workflows/write-a-first-touch-email-from-what-they-read.md)
  — the same reading step, for people who were never customers.
- [Protect your margin when you change prices](./protect-your-margin-when-you-change-prices.md)
  — the discount reflex, examined properly.
