Nudges — messages to a browsing visitor
Everything else in Stitchwork points at your staff. A nudge points at the shopper: a short message across the top of your own site, shown to a visitor whose browsing matched a rule you wrote, while they are still in the session — and measured against a control group that saw nothing, so you can tell whether it did anything.
Nudges are one of the optimisation tools: part of Collection, and an add-on to Independent. On a plan without them your site shows no nudges and the rules you wrote are kept for when they are switched on; tracking carries on either way.
What a nudge is
One line of text, your own words, shown on your own storefront to a visitor who matches a rule. It can ask for an email address, ask for a phone number, point at a page, or just say the thing and get out of the way. It works on visitors who have never told you who they are, which is most of them.
Your tracking tag renders it. There is no second script to paste and nothing to add to your theme — if the tag is installed and consented, nudges work.
What it is not:
- Not live chat. Nobody has to be sitting at a screen. The same rule works on one visitor or ten thousand, at 3am.
- Not an email tool. Nothing is sent. The message exists only in the session it appears in.
- Not a popup builder. One bar, one line, no modal, no overlay across the page, no second banner competing with your consent notice.
A nudge is the front end of the same loop the rest of the product runs. The visitor profile behind recommendations is what a rule reads. A phone number typed into a nudge opens a follow-up, so somebody owes that person a call. An email typed into one runs the same identity stitching as every other identification, so their whole prior trail joins a customer record.
Every nudge holds people back
This is the part worth reading before you write one, because it is the part that decides whether the report means anything.
The obvious version of this feature cannot be falsified. Show a banner to interested people, watch some of them buy, credit the banner — when interested people were always the ones most likely to buy. A conversion rate measured on the people who saw it is a measurement of your targeting, and it would look impressive for a nudge that rendered a blank bar.
So a visitor who matches your rule is assigned an arm before anything renders:
| Arm | What happens |
|---|---|
shown |
They match, and they see the message. |
holdout |
They match, and they see nothing at all. |
Both arms are recorded. Lift is the difference between them — the same people, wanting the same things, split by nothing except whether the bar appeared. That is the only number in this feature that is about the message rather than about who you aimed it at.
The default holdout is 10% — one matching visitor in ten. You can set anything from 0% to 50%. A bigger holdout settles the question faster and costs more showings; 0% turns measurement off entirely, and the report then says so rather than quietly reporting the treated arm's conversion rate as a result.
A held-back visitor receives exactly what a visitor who matched nothing receives: no message, no flag, no marker in the response. There is nothing on the page to inspect.
The arm is derived from the nudge and the visitor's own token, so a refresh cannot move somebody from control into treatment — and it is written down as well as derived, so a visitor assigned under a 10% split keeps reporting under it after you change the split.
Writing one
Settings → Nudges. A nudge has five parts:
| Field | Notes |
|---|---|
| Name | Internal only. The shopper never sees it. "Stalled bed browsers" beats "Nudge 4" in three weeks' time. |
| Message | One line, 240 characters maximum. It has to read across the top of a page. |
| What to ask for | Nothing, an email, a phone number, or a button. See below. |
| Who sees it | The rule. The form asks for four numbers and a yes/no; the API takes the full grammar. |
| Holdout | How many matching visitors are held back to measure against. Default 10%. |
A nudge is draft, live or paused. Draft is the
default — Save as draft writes it without showing it to anybody, and
Save & turn on puts it live. You can give one a start date, an end
date, or both, which is the difference between a promotion that stops on time and one a
customer tells you about a fortnight later.
Write it the way you would say it across a shop floor. A message that reads as "we can see exactly what you have been looking at" lands badly, however true it is. "Still deciding? Our bed specialist can call you back" says the same thing from the customer's side.
Start from your own buyers, not a guess
Writing your first nudge means picking numbers — how many product views counts as interested, how long a pause counts as stalling — with nothing to base them on. A guess produces a rule that fires on everybody or on nobody, and both look like the feature not working.
Seed from my buyers answers it from your own history. It measures what customers who actually bought had done before their first paid order — how many products they had looked at, across how many distinct items, over how many visits — and fills the thresholds in with those numbers. It prints the sentence saying how many customers it learned from.
Below 25 converting customers in the window it returns the shipped defaults and says so. Percentiles over a dozen orders are noise wearing a number's clothes.
Seeding sets the thresholds in a rule. It does not train a scorer, and it does not touch how Stitchwork decides who is interested. That matters because the ranking engine on your storefront and the lead list on your shop floor have to agree about what "interested" means — a second model here would be a third opinion that could disagree with both and explain itself to neither. What comes back is an ordinary rule set you can edit by hand, and it behaves identically to one you typed yourself.
What you can ask for
| CTA | What it does |
|---|---|
none |
A statement, with a dismiss control. Often the least intrusive thing available — and the cheapest thing to test a message with. |
button |
A label and a destination. Booking a video call, a collection slot, a size guide. The URL must start http:// or https://, checked when you save it and again in the database — it ends up as a link in your own page. |
email |
An address, routed to the same door every other identification goes through. The browser's entire prior trail attaches to a customer record retroactively and appears on their timeline. |
phone |
A callback request. Where the browser is already stitched to a customer this opens a follow-up — somebody now owes that person a call. Where it is not, it joins the callback queue. |
An email or phone CTA should carry a confirmation line — shown once they submit. Without one the bar simply disappears and they type it in again.
Where the browser has no customer record yet, the number is held rather than turned into a customer. A Stitchwork customer record requires an email address, and inventing a placeholder one to route around that is not a decision a storefront banner should make quietly.
These land in the callback queue on
Leads — the number, how long they have waited, and what they were
looking at. Somebody rings them, takes an email on the call, and claims it into a real
customer and a real follow-up. The nudge report counts what is still waiting as
callbacks_unclaimed; the queue is where you actually work them.
Answering twice is one answer. A shopper who submits, reloads and submits again does not open two follow-ups or create two customer records.
Who sees it
A rule is conditions over facts you can count. There is no intent score, no engagement stage and no interest ranking to target on — nothing where the platform decided something on your behalf and you would have to take our word for the result. If a visitor matches what the conditions say, they see the message; if they do not, they do not. The settings form is a builder over this same grammar, and every rule is read back as a plain sentence, so nothing anywhere re-derives what a rule means.
{"all": [
{"fact": "who.identified", "op": "is_false"},
{"fact": "history.product_views", "op": "gte", "value": 6, "within_days": 14},
{"fact": "history.products", "op": "gte", "value": 3, "within_days": 14},
{"fact": "history.orders", "op": "eq", "value": 0, "within_days": 365},
{"fact": "visit.seconds_since_action", "op": "gte", "value": 90}
]}
Read that back and it says what it does: somebody we cannot email, who has looked at six products across three lines in the last fortnight, has never ordered, and has now stopped moving. That last clause is the one worth copying — it stops the bar interrupting somebody who is already busy buying, the single moment a message across the top of the page can cost you a sale.
Each history.* condition carries its own within_days, between
1 and 365, and a condition without one is refused rather than given a default. A window
nobody chose is a rule nobody can read: "viewed 3 products" means something very
different over a day than over a year, and a returning customer would clear a long
window before they had done anything at all today.
Per condition rather than per nudge, because the useful rules need two windows at once — "looked at three beds this week and has not ordered anything in a year" is one rule.
The facts a rule can name
| Fact | Means |
|---|---|
everyone | Every visitor. Targeting everybody is available, but you have to say so. |
| Who they are | |
who.identified | We have an email address for them. |
who.source | utm_source of their most recent campaign touch, or the ad platform behind the click. |
who.medium | utm_medium. |
who.campaign | utm_campaign. |
| This visit — counted by the tag. A visit is a browser tab. | |
visit.page_views | Pages viewed in this visit. |
visit.product_views | Product pages viewed in this visit. |
visit.products | Different products viewed or basketed in this visit. Counted in the browser from the codes your storefront sends, so two variants of one product count as two here where history.products resolves them against your catalogue and counts one. |
visit.carts | Things added to the basket in this visit. |
visit.searches | Searches run in this visit. |
visit.seconds_since_action | Seconds since they last clicked, typed, scrolled or tapped. |
visit.path | Path of the page they are on now. |
Their history — every one of these needs within_days. | |
history.product_views | Product pages viewed. |
history.products | Different products viewed or added to the basket. |
history.carts | Things added to the basket. |
history.searches | Searches run. |
history.purchases | Purchases your website reported through the tag. |
history.orders | Orders on their customer record. Kept separate from purchases on purpose: one is what the site said, the other is what your books say, and which you mean is your call. |
history.quotes | Quotes raised on their customer record. |
history.categories | Categories of products they viewed or basketed — the raw set, not a ranking (a list). |
history.brands | Brands of products they viewed or basketed (a list). |
history.days_since_first_seen | Days since we first saw this browser. Not windowed. |
history.days_since_last_seen | Days since we last saw this browser. Not windowed. |
Each fact has a type, and the type decides its operators. Numbers take eq,
ne, gt, gte, lt, lte;
text takes eq, ne, in, not_in,
starts_with; lists take includes, not_includes;
yes/no facts take is_true, is_false. An operator the fact cannot
answer is refused when you save it rather than quietly matching nobody for ever.
Conditions combine under all or any.
GET /api/v1/nudges returns the facts, their types and the operators each one
accepts, so a form of your own can build itself from the contract instead of duplicating
it.
An unknown fact, an unknown operator, a malformed condition and an empty rule set all evaluate to no match — never to a match, and never to an error. The direction is deliberate: the failure mode of "unrecognised means true" is a broadcast to every visitor you have, caused by a typo.
A history.* condition with no within_days, or one naming a
window nothing was counted for, is in the same category: no match. We would rather
answer nothing than answer a question you did not ask.
Facts under visit. are reported by the browser and treated as such — they
are clamped, and the worst a forged one can do is show somebody a message meant for a
different sort of shopper. Every history. and who. fact is
computed on our side from the visitor token and cannot be influenced from the page.
How often it appears
| Frequency | Behaviour |
|---|---|
once_per_visitordefault | Once, ever. The safest setting and the right one for most messages. |
once_per_session | At most once in any 24 hours. A server cannot see a session boundary, so this is what it honestly means. |
every_n_days | At most once every n days, 1–365. |
Three things stop a nudge coming back regardless of frequency:
- They dismissed it. That is an answer.
- They responded to it. Asking again is nagging.
- They are in the holdout. A held-back visitor stays held back — re-evaluating them would leak your control group into the treated arm one page view at a time.
Where several live nudges match the same visitor, the highest priority wins and only one is shown. A visitor never sees two at once.
What it does to your page
Your storefront is not ours to reflow, so the bar overlays rather than pushing the page down. Covering a sticky header for a few seconds is a smaller risk than reflowing a layout we did not write, possibly mid-checkout.
- It lives in a shadow root. Your CSS cannot break it and it cannot break your CSS.
- It is built from text, never from markup. The message is set as text content, so nothing typed into a nudge can execute in a shopper's browser.
- It waits for consent. Exactly the same gate every tracked event waits behind — including your own CMP and Global Privacy Control. Consent withdrawn mid-visit takes the bar down with it.
- It removes itself rather than render broken. If anything fails our end, the shopper sees nothing.
- It never delays your page. The tag asks about a nudge after it has done its own work, on a passive schedule, and stops asking after a few minutes on a page.
A visitor with no __sw_v cookie — no consent, first request, cookies
cleared — is a visitor Stitchwork has nothing to target on, and gets no nudge. Nudges
are for the second half of a visit, not the first second of it.
Reading the report
Every nudge row has a report behind it. It answers one question — did the message do anything? — and it is written to be capable of answering "we cannot tell yet".
| Verdict | Means |
|---|---|
counting | Fewer than 30 visitors in one of the arms. No verdict is offered, and the note says how many each arm has. |
unmeasured | The holdout is 0%. There is no control group, so no lift figure is offered at all. |
better | The shown arm converted more, at 95% confidence. |
worse | The shown arm converted less, at 95% confidence. This is a real result and the report will say it. |
inconclusive | A difference, not a confident one — and the note says how many more visitors per arm would settle a difference that size. |
no_difference | Both arms converted at the same rate. |
Alongside the verdict:
- Responses — how many were shown it, dismissed it, answered it, how many follow-ups it opened, and how many callbacks are waiting on a name.
- Revenue, per arm and per currency. Read it beside the lift, never instead of it: this is revenue that followed a match, not revenue the nudge caused. As everywhere else in Stitchwork, currencies are never added together — there is no exchange rate in the system, so a combined total would be invented.
That is the whole reason the holdout exists, and the report is built to be able to say
so. A nudge reporting worse is more useful than one reporting a 200%
conversion rate on the treated arm, because somebody will spend money on the second
one.
The API
The tag calls these for you. They are documented for headless storefronts and native apps, where there is no page for our script to run in.
Query: visitor_token (required), path, seconds_since_action, and this visit's tally — page_views, product_views, products, carts, searches. CORS-enabled with OPTIONS preflight.
Returns {nudge: {id, message, cta_type, cta_label, cta_url, confirm_text}}, or {nudge: null}. No match, a holdout, an unknown token and an absent token all return the same empty answer — so this endpoint cannot be used to work out whether a token is live, or whether a visitor is in the control group.
This is a GET that writes: matching assigns an experiment arm. Responses are no-store, private and must never be cached — the arm and the frequency cap both live server-side. Any internal failure degrades to {nudge: null} rather than an error in your page.
Body: visitor_token, nudge_id, kind — one of click, email, phone, dismiss — and value for the two that carry one. Returns 204 and nothing else, for the same reason /identify does: anyone can type an address into a box on a public page, so an answer that confirmed anything about the customer behind it would be readable with a key that ships in your page source.
The visitor token is the credential. A response can only ever move the row belonging to the browser holding that token, so the arms cannot be stuffed from a script.
Rate limited per workspace per minute (2000 by default; ask us if your traffic needs more). Authoring, reporting and seeding are done from Settings → Nudges — those screens authenticate with your signed-in session, so there is no credential to hold and nothing to call them with from outside.
What a nudge can never do
- Reach a visitor who has not consented. Same gate as every event.
- Tell the browser anything but the message. Never the rule that
matched, never the profile behind it, never how many other nudges were considered.
Your
pk_livekey ships in your page source, so everything this endpoint returns is readable from any console on your site — and it is written on that assumption. - Be put in front of a shopper by an AI connection. An MCP agent can propose a nudge — it arrives in your list as a draft, labelled with the agent's name, and a draft cannot render. Turning it on is your act, in Settings → Nudges, audited like every other nudge write; the agent cannot activate, edit, pause or delete anything, its own proposals included. The label survives activation, so a running nudge a machine drafted still says so.
- Be deleted, once it ran. A nudge that ran is evidence about what visitors were shown. Pause it or end it; the record of what it said stays. The one exception is a draft no visitor was ever matched against — a proposal you are rejecting, or a form you abandoned — which can be deleted outright, because a nudge nobody saw is evidence of nothing.
Every write to a nudge is recorded in your audit log with who made it — a note is internal, but a nudge is text shown to a stranger on your storefront, and "who turned this on" is a question that gets asked eventually. A proposal is audited under the agent's own name, and the approval under yours.