Currency

ripllo.abandoned_cart.recovered.v1

Fires when a previously-abandoned cart is recovered — the buyer came back and completed a checkout that originated from a reminded cart. The primary metric for "did the recovery email pay off".

Reserved — not currently emitted in v1. markRecovered stamps recoveredAt on the reminder row but writes no outbox row, so nothing is dispatched to your endpoints. Subscribe defensively; for now, poll GET /abandoned-cart/stats or GET /abandoned-cart/reminders (rows with a non-null recoveredAt), or react to your own /recover call's {recovered: true} response.

When it will fire

When POST /api/v1/abandoned-cart/recover sets recoveredAt on a reminder row.

/recover is not idempotent on (accountId, checkoutSessionId). It matches the most recent unrecovered reminder for (accountId, customerId) whose sentAt is inside a hard-coded 72-hour window, and only stores checkoutSessionId on the row afterwards. A retried partner-platform webhook carrying the same checkoutSessionId therefore marks the next unrecovered reminder for that customer recovered as well, inflating the recovery count — and, once the event ships, would emit twice. Dedupe on the partner side: call /recover once per completed checkout.

If the partner platform calls /recover for a checkout that has no matching reminder (the cart was never abandoned, never reminded, or the reminder is older than 72 hours), nothing is stamped — the recovery endpoint responds with recovered: false and Ripllo is silent.

Payload

The intended shape, for when this ships (ids are opaque cuids — AbandonedCartReminder has no status column, and no delivery state is tracked):

{
  "id": "clx2k9f0a0002v8pq5w1kghi",
  "type": "ripllo.abandoned_cart.recovered.v1",
  "createdAt": "2026-05-13T10:43:22.187Z",
  "accountId": "acc_01HX...",
  "data": {
    "reminder": {
      "id": "clx2k9f0a0000v8pq7h3m1abc",
      "accountId": "acc_01HX...",
      "customerId": "cus_storlaunch_4471",
      "cartId": "cart_storlaunch_19823",
      "email": "alice@example.com",
      "valueAtSend": 250000,
      "currencyAtSend": "IDR",
      "discountCodeId": "clx2k9f0a0003v8pqz0p4jkl",
      "sentAt": "2026-05-13T06:42:00.000Z",
      "recoveredAt": "2026-05-13T10:43:22.140Z",
      "recoveredBySessionId": "cs_storlaunch_01HX...",
      "externalSource": "storlaunch",
      "externalRef": "cart_storlaunch_19823"
    },
    "checkoutSessionId": "cs_storlaunch_01HX...",
    "timeToRecoveryMs": 14482140
  }
}

The timeToRecoveryMs is a convenience: the milliseconds between sentAt and recoveredAt. Useful for histograms without doing date math in your handler.

Handler examples

// Node
if (event.type === 'ripllo.abandoned_cart.recovered.v1') {
  const { reminder, checkoutSessionId, timeToRecoveryMs } = event.data;
  analytics.track('cart_recovered', {
    customerId: reminder.customerId,
    valueIdr: reminder.valueAtSend,
    discountUsed: reminder.discountCodeId !== null,
    hoursToRecovery: Math.round(timeToRecoveryMs / 3_600_000),
    checkoutSessionId,
  });
}
# Python
if event['type'] == 'ripllo.abandoned_cart.recovered.v1':
    reminder = event['data']['reminder']
    analytics.track('cart_recovered',
        customer_id=reminder['customerId'],
        value_idr=reminder['valueAtSend'],
        discount_used=reminder['discountCodeId'] is not None,
        hours_to_recovery=round(event['data']['timeToRecoveryMs'] / 3_600_000),
    )
// Go
if event.Type == "ripllo.abandoned_cart.recovered.v1" {
    var data struct {
        Reminder            ripllo.AbandonedCartReminder `json:"reminder"`
        CheckoutSessionID   string                       `json:"checkoutSessionId"`
        TimeToRecoveryMs    int64                        `json:"timeToRecoveryMs"`
    }
    _ = json.Unmarshal(event.Data, &data)
    analytics.Track("cart_recovered", data.Reminder.CustomerID, data.Reminder.ValueAtSend, data.TimeToRecoveryMs)
}

What to do

  • Update recovery analytics. Once the event ships it's the push counterpart to GET /abandoned-cart/stats, which is where recovery rate comes from today. Aggregate by discountCodeId !== null to compare bundled-discount vs no-discount recovery.
  • Optionally thank the customer. Sending a "thanks for coming back!" follow-up can deepen engagement, but tread carefully — too many emails will provoke an unsubscribe.
  • Compute true recovery value. Compare reminder.valueAtSend (cart size at abandonment) to the actual checkout total (from the partner platform's order data) — the buyer may have added or removed items between abandonment and recovery.
  • Stop any "second reminder" sequences. If you've layered automations on top of Ripllo's first reminder, this is the cancel signal.

Common pitfalls

  • Counting valueAtSend as recovered revenue. It's the cart-snapshot value at abandonment time, not the final checkout total. The two diverge if the buyer added or removed items. For revenue reporting, use the partner platform's order-completed data and cross-reference by checkoutSessionId.
  • Treating recovery as instant. The buyer might come back days later. timeToRecoveryMs can easily span hours or days; bucket appropriately when reporting.
  • Assuming the reminder caused the recovery. Attribution is "the buyer was reminded and then completed a checkout". Some of those buyers would have come back anyway. The honest measure is incremental recovery rate vs a holdout group, which Ripllo doesn't currently A/B-test for you — you'd need to design that on the partner-platform side.
  • Sending the recovery event back to the partner. This event is for you (the integration owner) to track. Don't bounce it back to the partner platform — the partner already knows the checkout completed.

Next

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.