Abandoned cart
The abandoned cart namespace is Ripllo's recovery surface for buyers who add items, leave checkout, and never come back. Three pieces: one per-workspace AbandonedCartConfig (delay, copy, attached discount), a journal of AbandonedCartReminder rows the cron writes when it sends an email, and the markRecovered hook your payment-success webhook calls. This page covers ripllo.abandonedCart on the Node SDK; for the HTTP surface and field tables, see API → Abandoned cart.
Namespace
ripllo.abandonedCart.getConfig()
ripllo.abandonedCart.updateConfig(input)
ripllo.abandonedCart.listReminders(params?)
ripllo.abandonedCart.stats(params?)
ripllo.abandonedCart.recordReminder(input)
ripllo.abandonedCart.markRecovered(input)
Two flows reach this namespace: the dashboard (read config, update config, look at stats) and the partner integration (Storlaunch's cart-abandoned cron calls recordReminder; Plugipay's payment-success hook calls markRecovered).
The sweep cron and the email send both live partner-side today. Ripllo owns the config, the reminder journal, the opt-out list and the unsubscribe-token check; it does not schedule anything and does not send mail. The partner decides a cart is stale, sends its own email, then stamps the fact here. Ripllo will take over sending once an email provider is wired.
Config
getConfig
const cfg = await ripllo.abandonedCart.getConfig();
console.log(cfg.enabled, cfg.delayHours, cfg.discountCodeId);
Returns the merchant's config or a default object if never configured. Always returns — no null shape.
updateConfig
await ripllo.abandonedCart.updateConfig({
enabled: true,
delayHours: 4, // 1–168 (one week max)
emailSubject: "You left something in your cart",
emailPreview: "Take 10% off — code WELCOME10 inside",
discountCodeId: 'dc_01HX...', // optional attached incentive
});
PATCH semantics — only the fields you pass are touched. Pass enabled: false to pause reminders without losing the rest of the configuration.
Those five keys plus
marketingCampaignIdare the whole config. The request schema is not strict, so any other key is stripped before the write — a PATCH carrying only unrecognised keys returns200having changed nothing at all. In particular there is nodelayMinutes, nosubject/preheader, nobodyTemplate, nomaxRemindersPerCartand nooptOutLinkEnabled— the email body is the partner's, since the partner is what sends it. Opt-out is not a config toggle either: it's a per-buyer suppression list under/api/v1/abandoned-cart/suppressions, always honoured.
delayHours is a whole number of hours between 1 and 168; emailSubject and emailPreview are capped at 200 characters each.
Reminders
listReminders
const { items } = await ripllo.abandonedCart.listReminders({ limit: 50 });
for (const r of items) {
console.log(r.email, r.valueAtSend, r.sentAt, r.recoveredAt);
}
Returns the most recent reminders, newest first. Use this for the activity feed in the dashboard.
recordReminder
const result = await ripllo.abandonedCart.recordReminder({
accountId: 'acc_<merchant>',
customerId: 'cus_<buyer>',
cartId: 'cart_<storlaunchCartId>',
email: 'buyer@example.com',
cartSnapshot: { items: [{ productId: 'p_001', quantity: 2, unitPrice: 125_000 }] },
valueAtSend: 250_000,
currencyAtSend: 'IDR',
discountCodeId: 'dc_01HX...',
externalSource: 'storlaunch',
externalRef: 'cart_<storlaunchCartId>',
});
if (result.reason === 'opted_out') {
// Buyer hit the opt-out link previously; nothing was sent.
}
Called from the partner's cart-abandoned cron after the email has been dispatched. Stamps a reminder row that the recovery stats roll up against. Returns { created: false, reason: 'opted_out' } if the buyer has previously opted out.
markRecovered
await ripllo.abandonedCart.markRecovered({
accountId,
customerId,
checkoutSessionId,
completedAt: new Date().toISOString(),
});
Called from the payment-success webhook. Matches the purchase against any outstanding reminders for this customer + cart, flips them to recovered, and contributes to the recovery-rate stat. Idempotent on checkoutSessionId.
Stats
const stats = await ripllo.abandonedCart.stats({ windowDays: 30 });
console.log(stats.remindersSent, stats.cartsRecovered, stats.recoveryRate, stats.recoveredValueAtSend);
Default window is 30 days, clamped to 365; pass windowDays to narrow or widen. The window is not echoed back in the response — keep the value you sent if you need to label the number. recoveredValueAtSend sums valueAtSend over recovered reminders (what the cart was worth when the reminder went out, not what the buyer eventually paid), and currency is taken from the first recovered reminder, so it's null until something recovers.
Types
interface AbandonedCartConfig {
id?: string;
accountId?: string;
enabled: boolean;
delayHours: number;
emailSubject: string;
emailPreview: string;
discountCodeId: string | null;
}
interface AbandonedCartReminder {
id: string;
accountId: string;
customerId: string;
cartId: string;
email: string;
valueAtSend: number;
currencyAtSend: string;
discountCodeId: string | null;
sentAt: string;
recoveredAt: string | null;
}
interface RecoveryStats {
remindersSent: number;
cartsRecovered: number;
recoveryRate: number; // 0..1
recoveredValueAtSend: number;
currency: string | null;
}
Common patterns
Wire the partner's cart-abandoned cron
// On Storlaunch — cron fires N minutes after last cart activity.
for (const cart of staleCarts) {
const cfg = await ripllo.abandonedCart.getConfig();
if (!cfg.enabled) break;
await sendEmailToBuyer({ to: cart.email, /* template + values */ });
await ripllo.abandonedCart.recordReminder({
accountId: cart.accountId,
customerId: cart.customerId,
cartId: cart.id,
email: cart.email,
cartSnapshot: cart.snapshot,
valueAtSend: cart.subtotal,
currencyAtSend: cart.currency,
discountCodeId: cfg.discountCodeId,
externalSource: 'storlaunch',
externalRef: cart.id,
});
}
Wire the payment-success hook
// On Storlaunch — Plugipay's `payment.succeeded` webhook handler.
await ripllo.abandonedCart.markRecovered({
accountId,
customerId,
checkoutSessionId,
completedAt: event.occurredAt,
});
Idempotency makes this safe to call on every payment event — the no-match case is a cheap no-op.
Errors
| Code | Status | Cause |
|---|---|---|
VALIDATION |
400 | Body doesn't parse; delayHours outside 1–168; discountCodeId doesn't exist in this workspace. |
NO_ACCOUNT |
403 | The authenticated principal carries no accountId. |
ACCOUNT_MISMATCH |
403 | recordReminder / markRecovered named an accountId that isn't the signed principal's. |
INSUFFICIENT_SCOPE |
403 | API key lacks read (on getConfig / listReminders / stats) or write (on everything else). |
recordReminder does not check enabled — it records whatever you stamp. Gate on cfg.enabled yourself before you send, as the pattern below does.
See Errors for handling.
Next
- Discount codes — the optional attached incentive.
- API → Abandoned cart — full HTTP reference.