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.
markRecoveredstampsrecoveredAton the reminder row but writes no outbox row, so nothing is dispatched to your endpoints. Subscribe defensively; for now, pollGET /abandoned-cart/statsorGET /abandoned-cart/reminders(rows with a non-nullrecoveredAt), or react to your own/recovercall'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 bydiscountCodeId !== nullto 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
valueAtSendas 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 bycheckoutSessionId. - Treating recovery as instant. The buyer might come back days later.
timeToRecoveryMscan 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.
Related events
ripllo.abandoned_cart.reminder_sent.v1— reserved-not-emitted; the "we sent the email" counterpart.ripllo.abandoned_cart.suppressed.v1— reserved-not-emitted; buyer opted out.ripllo.discount_code.redeemed.v1— if the recovery used the bundled discount code, this event fires too.
Next
- Abandoned cart resource — the full config + recovery API.
- Webhooks reference — envelope, retries, signature verification.