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