Abandoned cart
The abandoned cart resource governs the recovery flow that emails buyers when they leave items in their cart without checking out. Ripllo owns the configuration, the suppression list, the reminder history, the recovery accounting, and the one-click public unsubscribe page.
It does not own the cart itself. The partner platform (Storlaunch) detects abandonment, snapshots the cart, and calls Ripllo to record the reminder. When the buyer eventually completes a checkout, the partner platform calls back to mark the cart recovered. Ripllo's role is to track configuration, deliver email through the merchant's configured channel, and compute recovery rate.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/abandoned-cart/config |
Get config |
PATCH |
/api/v1/abandoned-cart/config |
Update config |
GET |
/api/v1/abandoned-cart/reminders |
Recent reminders |
GET |
/api/v1/abandoned-cart/stats |
Recovery stats |
GET |
/api/v1/abandoned-cart/suppressions |
List suppressions |
POST |
/api/v1/abandoned-cart/suppressions |
Add a suppression |
DELETE |
/api/v1/abandoned-cart/suppressions/:email |
Remove a suppression |
POST |
/api/v1/abandoned-cart/reminders |
Record a reminder (partner platform) |
POST |
/api/v1/abandoned-cart/recover |
Mark a cart recovered (partner platform) |
GET |
/api/v1/abandoned-cart/unsubscribe |
Public unsubscribe page (HTML, no auth) |
Get config
GET /api/v1/abandoned-cart/config
Returns the merchant's abandoned-cart config. If no config exists yet, Ripllo returns the default stub so the dashboard never has to special-case "first visit":
{
"data": {
"enabled": false,
"delayHours": 4,
"emailSubject": "You left something in your cart",
"emailPreview": "Come back to finish your order",
"discountCodeId": null
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Update config
PATCH /api/v1/abandoned-cart/config
Partial update. Sends only the fields you want to change.
Request body
| Field | Type | Notes |
|---|---|---|
enabled |
boolean | Master on/off. When false, partner-side calls to /reminders succeed but no email is dispatched. |
delayHours |
integer (1–168) | How long after detected abandonment to send the reminder. Default 4. 168 = 7 days. |
emailSubject |
string (≤200) | Email subject line. Liquid-style {{ first_name }} placeholders supported. |
emailPreview |
string (≤200) | Preview text shown in the inbox before the email is opened. |
discountCodeId |
string | null | A dc_… ID to inject into the reminder email as a one-click apply link. Must exist and be active in this workspace. Pass null to remove. |
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION |
Shape wrong, delayHours out of range, supplied discountCodeId not found in this workspace. |
await ripllo.abandonedCart.config.update({
enabled: true,
delayHours: 6,
emailSubject: '{{ first_name }}, your cart is still waiting',
discountCodeId: 'dc_01HX...',
});
Recent reminders
GET /api/v1/abandoned-cart/reminders
Returns the most recent reminder rows for the merchant, newest first. Useful for dashboard "recent activity" widgets and for manually verifying delivery during setup.
Query parameters
| Param | Default | Notes |
|---|---|---|
limit |
50 |
Clamped to [1, 200]. |
Response
{
"data": {
"items": [
{
"id": "acr_01HX...",
"accountId": "acc_01HX...",
"customerId": "cus_01HX...",
"cartId": "cart_storlaunch_19823",
"email": "alice@example.com",
"valueAtSend": 250000,
"currencyAtSend": "IDR",
"discountCodeId": null,
"status": "delivered",
"recoveredAt": null,
"externalSource": "storlaunch",
"externalRef": "cart_storlaunch_19823",
"sentAt": "2026-05-13T10:42:00.000Z",
"createdAt": "2026-05-13T10:42:00.000Z"
}
]
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Recovery stats
GET /api/v1/abandoned-cart/stats
Aggregate metrics over a rolling window.
Query parameters
| Param | Default | Notes |
|---|---|---|
windowDays |
30 |
Clamped to [1, 365]. |
Response
{
"data": {
"windowDays": 30,
"remindersSent": 412,
"cartsRecovered": 87,
"recoveryRate": 0.211,
"recoveredValueIdr": 18230000
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
recoveryRate is cartsRecovered / remindersSent rounded to 3 decimals. recoveredValueIdr is the sum of valueAtSend for reminders whose recoveredAt falls within the window — not the eventual checkout total, which can differ if the buyer added items between abandonment and recovery.
List suppressions
GET /api/v1/abandoned-cart/suppressions
Returns buyers who have opted out of abandoned-cart emails. The list is per-merchant: an opt-out at one merchant doesn't affect any other. Newest first.
Query parameters
| Param | Default | Notes |
|---|---|---|
limit |
200 |
Clamped to [1, 500]. |
Response
{
"data": {
"suppressions": [
{
"id": "bep_01HX...",
"accountId": "acc_01HX...",
"email": "bob@example.com",
"abandonedCartOptOut": true,
"optedOutAt": "2026-05-12T15:33:00.000Z"
}
]
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Add a suppression
POST /api/v1/abandoned-cart/suppressions
Manually suppress a buyer (typically after a "stop emailing me" support ticket). Same as the public unsubscribe page, but with merchant credentials so it shows up in the audit log under the operating user.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | RFC-valid email. Normalised to lowercase before insert. |
Returns 201 Created with the BuyerEmailPreference row.
Remove a suppression
DELETE /api/v1/abandoned-cart/suppressions/:email
Lifts the suppression. The buyer will be eligible for future reminders again. The row is not deleted — abandonedCartOptOut is flipped to false so the historical opt-out remains queryable.
await ripllo.abandonedCart.suppressions.remove('alice@example.com');
Record a reminder
POST /api/v1/abandoned-cart/reminders
No auth. Called by the partner platform when a cart times out. Snapshots the cart and (if config.enabled) dispatches the reminder email via the merchant's configured email channel.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
accountId |
string | yes | Merchant. |
customerId |
string | yes | The buyer's Ripllo customer ID. |
cartId |
string | yes | Partner's cart identifier. Idempotency key — repeating the same (accountId, cartId) returns the existing reminder row, no double-send. |
email |
string | yes | Where to send the reminder. |
cartSnapshot |
any (object) | yes | Free-form JSON snapshot of the cart at abandonment time. Used to render the reminder email body. |
valueAtSend |
integer | yes | Cart subtotal at snapshot time, in smallest currency unit. |
currencyAtSend |
string | yes | ISO 4217. |
discountCodeId |
string | no | Override the config-level code for this one reminder. |
externalSource |
string | no | Recommended: "storlaunch". |
externalRef |
string | no | Partner's own cart/order ID. Convention: same as cartId. |
Response
{
"data": {
"id": "acr_01HX...",
"status": "queued",
"duplicate": false
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
duplicate: true indicates this (accountId, cartId) already had a reminder — no new email was sent.
If the buyer is on the suppression list, the reminder row is created with status: "suppressed" and no email is sent. This is intentional: the row exists for accounting (so "carts abandoned" doesn't lie), but the buyer's stated preference is honoured.
Mark a cart recovered
POST /api/v1/abandoned-cart/recover
No auth. Called by the partner platform when a buyer completes a checkout that originated from a reminded cart. Idempotent on (accountId, checkoutSessionId).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
accountId |
string | yes | |
customerId |
string | yes | |
checkoutSessionId |
string | yes | Partner's session ID. |
completedAt |
ISO 8601 | no | When the checkout completed. Defaults to "now" at request receipt. |
Response
{
"data": {
"recovered": true,
"reminderId": "acr_01HX...",
"cartId": "cart_storlaunch_19823"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
If no matching reminder exists (the checkout was for a cart that was never abandoned), recovered: false is returned with reminderId: null. The partner platform can call /recover on every checkout without filtering — it's cheap and idempotent.
Public unsubscribe
GET /api/v1/abandoned-cart/unsubscribe?accountId=acc_…&email=…&token=…
Renders HTML. Public, no auth. The one-click unsubscribe target from the link in every reminder email. The token is an HMAC of (accountId, email) with the per-merchant unsubscribe secret; tampered links return a 401 page.
This endpoint is the user-facing equivalent of the merchant-facing POST /suppressions. On success, the buyer sees a confirmation page.
The unsubscribe URL is constructed by Ripllo when it dispatches the reminder — you don't generate these yourself. They're documented here so you understand what they do when reviewing email templates.
The objects
AbandonedCartConfig
| Field | Type | Notes |
|---|---|---|
id |
string | acc_acg_ + ULID (the prefix avoids collision with accountId). |
accountId |
string | Owning workspace. Unique. |
enabled |
boolean | Master switch. |
delayHours |
integer | 1–168. |
emailSubject, emailPreview |
string | Liquid-supported template strings. |
discountCodeId |
string | null | The bundled discount code. |
AbandonedCartReminder
| Field | Type | Notes |
|---|---|---|
id |
string | acr_ + ULID. |
accountId, customerId |
string | |
cartId |
string | Partner's ID. Unique per (accountId, cartId). |
email |
string | |
cartSnapshot |
object | Free-form JSON. |
valueAtSend, currencyAtSend |
integer / string | Snapshot at abandonment time. |
discountCodeId |
string | null | The code bundled into this specific reminder. |
status |
enum | queued, delivered, failed, suppressed. |
recoveredAt |
ISO 8601 | null | Set on /recover. |
externalSource, externalRef |
string | null | Partner anchors. |
sentAt, createdAt |
ISO 8601 |
BuyerEmailPreference
| Field | Type | Notes |
|---|---|---|
id |
string | bep_ + ULID. |
accountId |
string | Per-merchant scope. |
email |
string | Lowercased. Unique per (accountId, email). |
abandonedCartOptOut |
boolean | Currently the only preference flag; more channels will land here as Ripllo gains them. |
optedOutAt |
ISO 8601 | null | When the opt-out happened. null for rows where abandonedCartOptOut is false. |
Events
| Event type | Fires on | Status |
|---|---|---|
ripllo.abandoned_cart.reminder_sent.v1 |
A reminder row transitions from queued to delivered. |
Reserved — not currently emitted. |
ripllo.abandoned_cart.recovered.v1 |
POST /abandoned-cart/recover sets recoveredAt for the first time. |
Emitted — the primary recovery signal. |
ripllo.abandoned_cart.suppressed.v1 |
A buyer is added to the suppression list. | Reserved — not currently emitted. |
Next
abandoned_cart.recoveredevent — wire format for the recovery signal.- Discount codes — the codes you can bundle into reminder emails.
- Channels — the merchant's email provider must be configured for reminders to dispatch.