Trigger reorder reminders from purchase events
This guide is the build companion to How can ecommerce brands automate reorder reminders for consumable products? and Reorder reminder message examples, which cover timing and wording.
How do reorder reminders work with purchase events?
A reminder after the customer has run out comes too late, and one a week after the purchase is noise. The due date in the event lets the journey send the reminder just before it's needed.
Order paid → product.purchased recorded → Journey waits until the due date → Reminder sent
Your store knows the product and the quantity, so it calculates the date. The journey owns the lead time and the message, so your marketing team can change both in the Flownally app.
Calculate the reorder date from the purchase
Keep the number of days one unit lasts with each consumable product, for example in a product field in your store. Multiply it by the quantity bought to get the reorder interval, and add the interval to the purchase date:
// Days one unit lasts, by SKU. Products without an entry get no reminder.
const DAYS_PER_UNIT = {
"CLEANSER-150ML": 30,
"VITAMIN-D-60": 60,
"COFFEE-BEANS-1KG": 21,
};
// Returns the consumable that runs out first, or null when the order has none.
export function reorderFor(order) {
const [first] = order.items
.filter((item) => DAYS_PER_UNIT[item.sku])
.map((item) => ({ item, intervalDays: DAYS_PER_UNIT[item.sku] * item.quantity }))
.sort((a, b) => a.intervalDays - b.intervalDays);
if (!first) return null;
const dueOn = new Date(order.paidAt);
dueOn.setUTCDate(dueOn.getUTCDate() + first.intervalDays);
return { ...first, dueOn: dueOn.toISOString().slice(0, 10) };
}The item that runs out first decides the date, so each order schedules one reminder instead of one per product.
Create the purchase event type
Create the event type once with Create event type. reorderDueOn uses "format": "date", which is what lets the journey wait for it.
curl -X POST "https://api.flownally.com/v1/event-types" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "product.purchased",
"description": "Customer bought a consumable product.",
"payloadSchema": {
"orderNumber": { "type": "string" },
"firstName": { "type": "string" },
"productName": { "type": "string" },
"productSku": { "type": "string" },
"quantity": { "type": "number" },
"reorderIntervalDays": { "type": "number" },
"reorderDueOn": { "type": "string", "format": "date" },
"reorderUrl": { "type": "string" }
}
}'payloadSchema is a simplified format, not a JSON Schema document. Each field has a type, an optional format (date or date-time), and is required unless it sets "required": false. Recover abandoned carts with events and journeys describes the format in full.
Record the purchase event when the order is paid
Find the contact by phone number and record the event with Create contact event. This example uses findOrCreateContact and recordEvent from the abandoned cart guide, which search the cf_phone field before creating a contact and treat 409 as already recorded.
import { findOrCreateContact, recordEvent } from "./flownally.js";
import { reorderFor } from "./reorder.js";
export async function recordPurchase(order) {
const reorder = reorderFor(order);
if (!reorder) return;
const contactId = await findOrCreateContact(order.phone, order.customerName);
await recordEvent(contactId, {
name: "product.purchased",
payload: {
orderNumber: order.number,
firstName: order.firstName,
productName: reorder.item.title,
productSku: reorder.item.sku,
quantity: reorder.item.quantity,
reorderIntervalDays: reorder.intervalDays,
reorderDueOn: reorder.dueOn,
reorderUrl: `https://shop.example.com/reorder/${order.id}`,
},
idempotencyKey: `product-purchased-${order.id}`,
});
}Here's the same request on its own, and the recorded event Flownally returns with 201:
curl -X POST "https://api.flownally.com/v1/contacts/$CONTACT_ID/events" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "product.purchased",
"payload": {
"orderNumber": "4827",
"firstName": "Anna",
"productName": "Hydrating Cleanser 150 ml",
"productSku": "CLEANSER-150ML",
"quantity": 1,
"reorderIntervalDays": 30,
"reorderDueOn": "2026-12-04",
"reorderUrl": "https://shop.example.com/reorder/4827"
},
"idempotencyKey": "product-purchased-4827"
}'{
"id": "evt_d7pjmuk9lfdk4tgvlnpg",
"tenantId": "tn_d7phk42ge9t9t6324f20",
"contactId": "con_d7pibvs6gljrc2pfdc1g",
"eventTypeId": "ety_d7pji5t0tajnmqt4a2rg",
"eventTypeName": "product.purchased",
"payload": {
"orderNumber": "4827",
"firstName": "Anna",
"productName": "Hydrating Cleanser 150 ml",
"productSku": "CLEANSER-150ML",
"quantity": 1,
"reorderIntervalDays": 30,
"reorderDueOn": "2026-12-04",
"reorderUrl": "https://shop.example.com/reorder/4827"
},
"idempotencyKey": "product-purchased-4827",
"createdAt": "2026-11-04T10:40:00Z"
}A payload that doesn't match the event type fails with 400. A contact or event type that doesn't exist fails with 404.
Build the reorder reminder journey
Build the journey in the Flownally app, following How to create a journey:
- Start on
product.purchased. Pick the event type in the Start block. - Wait until the due date. Add a Wait block that continues on a specific date, choose
reorderDueOnfrom the event, and set an offset such as three days before the date. - Send the reminder. Add a Send message step with a WhatsApp template that uses
firstName,productNameandreorderUrl.
Customer replies to the reminder arrive in the Flownally inbox, so your team can answer questions about sizes, flavors or timing.
How do I send a reminder for every purchase?
A journey's entryMode decides how often a contact can enter it:
once. A contact enters at most once, ever, so only the first purchase gets a reminder.repeatable. A contact enters again after the previous run ends, so a purchase made while they wait for an earlier reminder doesn't start its own run.concurrent. A contact enters on every matching event, even while earlier runs are still waiting.
For reorder reminders, concurrent gives every purchase its own reminder. Set it with Update journey, then publish and activate the journey again, because entry changes reach contacts only from the next active version:
curl -X PATCH "https://api.flownally.com/v1/journeys/$JOURNEY_ID" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "entryMode": "concurrent" }'
curl -X POST "https://api.flownally.com/v1/journeys/${JOURNEY_ID}:publish" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN"
curl -X POST "https://api.flownally.com/v1/journeys/${JOURNEY_ID}:activate" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN"See Publish journey and Activate journey.
Schedule reorder reminders from your store instead
A waiting run doesn't know about later orders, so a customer who reorders early still gets the reminder for the earlier purchase. If that happens often, keep the schedule in your store.
Daily job finds due orders → Skips customers who reordered → Records reorder.due → Journey sends right away
- Save the due date. Store
reorderDueOnwith the order. - Run a daily job. Find orders whose reminder date is today, and skip customers who reordered since.
- Record
reorder.due. Use the same payload fields asproduct.purchased. - Send without waiting. Start the journey on
reorder.dueand send the template right away.
Your store then decides who gets a reminder, and the journey still owns the message.
FAQ
Why didn't a second purchase get its own reminder?
The journey most likely uses repeatable, which lets a contact enter again only after their previous run ends. A purchase recorded while the contact waits for an earlier reminder doesn't start a run. Set entryMode to concurrent, then publish and activate the journey.
Why did a customer who reordered early still get a reminder?
The run for the earlier purchase keeps waiting for its own date, and later orders don't change it. If early reorders are common, schedule reminders from your store and record reorder.due only for customers who haven't reordered.
Why can't I choose reorderDueOn in the Wait block?
The Wait block offers the date fields of the event types selected in the Start block. Check that the journey starts on product.purchased, and that the event type declares reorderDueOn as a string with "format": "date" or "date-time".
What date format does reorderDueOn need?
Send YYYY-MM-DD, such as 2026-12-04, for a date field. Use a date-time field instead if you need a time of day.