> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an abandoned-cart recovery flow

> Connect a Shopify or WooCommerce cart event to Orbit Flows, wait two hours, check whether the customer purchased, and recover the cart with SMS and a WhatsApp fallback.

# Build an abandoned-cart recovery flow

An abandoned-cart flow should do three things in order: receive a cart event, give the customer time to finish checkout, and stop as soon as an order is paid. This guide assembles that pattern in Devotel Orbit with a two-hour delay, an order check, an SMS message, and a WhatsApp fallback that includes the product link.

You can use the same flow with Shopify, WooCommerce, a custom storefront webhook, a CDP import, or a scheduled re-scan. The examples use flat flow variables such as `{{first_name}}` and `{{recovery_url}}`; dotted placeholders such as `{{contact.first_name}}` do not resolve in flow messages.

## 1. Pick the use case

The graph is the same for several incomplete-transaction signals. Change the event name, conversion signal, and message copy to fit your business:

| Use case | Start signal | Conversion signal | Useful Orbit capability |
| - | - | - | - |
| **Abandoned cart** | `cart.created` or your store's cart event | `order.paid` | [CDP segments](/guides/cdp-segments), inbound events, and a Flow `transform` node for normalized product data |
| **Abandoned quote** | `quote.created` or a CRM webhook | `quote.accepted` or `order.paid` | A webhook-triggered flow can map the quote payload before the delay |
| **Incomplete KYC** | `kyc.started` | `kyc.completed` | A segment or event source can identify incomplete applications without sending until the condition still holds |

Start with a single contact-level conversion key. For a cart, that is usually a checkout ID or cart ID. Include it in both the abandoned event and the purchase event so the condition cannot accidentally match a different order from the same customer.

## 2. Wire the source event

Choose one source. All three approaches end with the same flat Flow context.

### Option A: Shopify or WooCommerce integration

Connect the store first, then subscribe the flow to the normalized cart event. Shopify's `checkouts/create` delivery emits `cart.created` with `contact_id`, `recovery_url`, `total_price`, and `currency`. See [Connect Shopify end to end](/guides/shopify-integration).

WooCommerce order events enter the CDP stream, and a cart-abandonment event can carry `recovery_url`, `cart_total`, and `line_items`. See [Connect WooCommerce end to end](/guides/woocommerce-integration). Map the store-specific event to the flow's stable keys with a `transform` node if your source uses different names.

### Option B: Send a webhook directly to a Flow

Use a **Webhook** trigger when your storefront already knows when a checkout is abandoned. Post an addressable contact and a stable cart key:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/flows/flow_cart_recovery/webhook" \
  -H "X-Flow-Signature: $FLOW_SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "cart.created",
    "contact_id": "contact_02a3f1c9d4e5",
    "cart_id": "cart_501126",
    "first_name": "Ada",
    "phone": "+14155550134",
    "whatsapp": "+14155550134",
    "recovery_url": "https://shop.example/cart/recover/cart_501126",
    "product_name": "Ceramic pour-over dripper",
    "cart_total": "79.98",
    "currency": "USD"
  }'
```

Treat a successful `202` as an accepted trigger, not as proof that a message was sent. Inspect the execution after the delay. Sign and authenticate the webhook according to [Webhook security](/webhooks/security), and make the event idempotent in your sender so a store retry does not enroll the same cart twice.

### Option C: Load or re-scan events through CDP

For a backfill or a scheduled export, use [CDP file ingest](/guides/cdp-file-ingest). Upload event rows in chunks of 200 or fewer and include the cart key in `properties`:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/cdp/file-ingest" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "json",
    "content_kind": "events",
    "content": "[{\"event\":\"cart.created\",\"userId\":\"cust_123\",\"cart_id\":\"cart_501126\",\"recovery_url\":\"https://shop.example/cart/recover/cart_501126\"}]",
    "dry_run": true
  }'
```

Run a dry run first, then ingest the corrected file. A scheduled re-scan can emit the same event to a schedule-triggered flow, but do not re-enroll a cart unless its stable cart key has changed or the earlier run has ended.

## 3. Build the wait, condition, and fallback

The canonical path is:

```text theme={null}
cart.created → normalize fields → wait 2 hours → still not purchased?
                                                    ├─ no  → end
                                                    └─ yes → SMS → WhatsApp if SMS fails
```

The condition must check the purchase signal for the **same cart**. Do not use only `contact_id`: a customer can have a new cart while an older cart is being recovered. The API definition below uses `cart_purchased` as the result of a condition lookup populated by your event mapping. If your event source exposes `order_status`, gate on `order_status != "paid"` instead.

[Functions node recipes](/guides/flows-transform-node-recipes) explains how to use a `transform` node to copy provider-specific keys into stable top-level variables. The [smart-send fallback guide](/guides/smart-send-fallback-chains) explains how `cascade_policy` arms a delivery fallback.

### Create the draft flow

This request creates a draft. Replace the example contact and sender values with values from your tenant. The `transform` node creates the message variables; the delay parks the execution; the condition exits when the cart is no longer eligible.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/flows" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Abandoned cart recovery",
    "trigger_type": "event",
    "definition": {
      "trigger": { "type": "event", "event": "cart.created" },
      "nodes": [
        {
          "id": "normalize_cart",
          "type": "transform",
          "data": {
            "assignments": [
              { "target": "cart_key", "template": "{{cart_id}}" },
              { "target": "cart_link", "template": "{{recovery_url}}" },
              { "target": "display_name", "template": "{{first_name}}" }
            ]
          }
        },
        { "id": "wait_two_hours", "type": "delay", "data": { "amount": 2, "unit": "hours" } },
        {
          "id": "check_purchase",
          "type": "condition",
          "data": { "field": "cart_purchased", "operator": "equals", "value": "false" }
        },
        {
          "id": "recover_sms",
          "type": "sendSms",
          "data": {
            "to": "{{phone}}",
            "body": "Hi {{display_name}}, your {{product_name}} is still in your cart: {{cart_link}} Reply STOP to opt out."
          }
        },
        {
          "id": "recover_whatsapp",
          "type": "sendWhatsApp",
          "data": {
            "to": "{{whatsapp}}",
            "body": "Hi {{display_name}}, you left {{product_name}} in your cart. Finish here: {{cart_link}}"
          }
        }
      ],
      "edges": [
        { "source": "normalize_cart", "target": "wait_two_hours" },
        { "source": "wait_two_hours", "target": "check_purchase" },
        { "source": "check_purchase", "sourceHandle": "yes", "target": "recover_sms" },
        { "source": "recover_sms", "sourceHandle": "error", "target": "recover_whatsapp" }
      ]
    }
  }'
```

The response is a draft flow. Save its `data.id`, then read it back before publishing:

```json theme={null}
{
  "data": {
    "id": "flow_cart_recovery",
    "name": "Abandoned cart recovery",
    "status": "draft",
    "definition": {
      "trigger": { "type": "event", "event": "cart.created" },
      "nodes": [
        { "id": "wait_two_hours", "type": "delay", "data": { "amount": 2, "unit": "hours" } }
      ]
    }
  },
  "meta": { "request_id": "req_7c1e2a", "timestamp": "2026-10-05T11:22:31.004Z" }
}
```

Fetch the saved definition with `GET /api/v1/flows/flow_cart_recovery`. If your tenant's flow builder uses the channel-specific fallback node rather than a node error edge, configure the same SMS → WhatsApp order in the builder and verify it in Test mode. A delivery fallback is not a second enrollment: it carries the remaining message context and product link to the next eligible channel.

### Publish and test

Publish only after you have tested both branches:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/flows/flow_cart_recovery/publish" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Send one test event with `cart_purchased: "false"` and confirm that the run is `waiting` during the delay. Send a second test with the matching cart key marked purchased and confirm that the condition takes the `no` path. Finally, force an SMS delivery failure in your tenant's test setup and confirm that the WhatsApp fallback keeps the same `cart_link`.

## 4. Personalize the message

Keep provider fields at the edge of the flow. Use the transform node to derive clean, flat values such as `display_name`, `product_name`, `cart_link`, and `cart_total`; downstream channel nodes should reference those values directly. The transform node can assign a literal, a template, or a safe boolean expression, but it cannot run arbitrary code.

Before publishing, preview the message against representative contacts. [Validate campaign personalization merge-tags](/guides/campaign-personalization-preview) describes the preview contract and coverage warnings. Check that:

* `phone` and `whatsapp` are present for the intended recipients.
* `recovery_url` is a valid HTTPS link owned by your store.
* The product name and price are present, correctly escaped, and short enough for the channel.
* Missing optional values have a safe message path; an empty `{{product_name}}` is worse than leaving the product name out.
* You use flat tags. `{{contact.first_name}}` will render blank; `{{first_name}}` resolves from the event/contact context.

If you need click measurement, create a tenant-owned short link before the flow or use the links API, then inject the resulting URL as `recovery_url`. Do not expose raw credentials or unrelated customer attributes in the event payload.

## 5. Connect Shopify or WooCommerce

For Shopify, connect the store through **Settings → Integrations**. The integration registers the checkout webhook and emits `cart.created` with a recovery URL. Start with the complete [Shopify integration walkthrough](/guides/shopify-integration), then use the flow definition in this guide for the two-hour wait and purchase check.

For WooCommerce, create a REST key pair in **WooCommerce → Settings → Advanced → REST API**, connect it in Orbit, and let the scheduled sync ingest cart and order events. Follow the [WooCommerce integration walkthrough](/guides/woocommerce-integration) for the key pair and event envelope. Map the WooCommerce cart event to `cart_id`, `recovery_url`, and the contact address before it enters the flow.

Both integrations are inbound sources. The SMS and WhatsApp sends use your tenant's configured messaging routes; do not send outbound traffic through a provider's control or messaging API that is not configured for your tenant.

## 6. Stop after purchase

A purchase must abort the waiting run before it sends. When the store emits `order.paid`, update or emit the matching cart context with `cart_purchased: "true"` and the same `cart_id`. The condition's `no` edge has no target, so the execution ends without a message.

Use the [Flows Executions API](/flows/executions) to verify the behavior:

```bash theme={null}
curl -G "https://api.orbit.devotel.io/api/v1/flows/executions" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  --data-urlencode "flow_id=flow_cart_recovery" \
  --data-urlencode "status=waiting"
```

Then fetch the execution trace:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/flows/executions/exec_cart_123" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Look for the `check_purchase` step and its rendered input. A purchased cart should show the false branch and no `recover_sms` step. A still-open cart should show the true branch, followed by the SMS step and, only if delivery reaches a terminal failure, the WhatsApp fallback.

If your store cannot update a waiting execution, use a purchase event as a second flow trigger that cancels the matching run through the Flows execution action exposed by your tenant. Keep the cancellation key scoped to `cart_id`; never cancel every flow for a contact because that can suppress a newer, valid cart.

## 7. Add the compliance gate

Compliance settings belong to your tenant. Configure the controls before you publish:

* **Quiet hours:** schedule the two-hour send for the recipient's permitted local window. If the delay ends during quiet hours, let the tenant send control defer the message rather than adding a second ad hoc scheduler.
* **Consent:** enroll only contacts whose tenant records show marketing consent for the channel. SMS consent does not automatically grant WhatsApp consent.
* **Unsubscribe:** include your tenant's approved unsubscribe footer on every SMS step. Configure the equivalent WhatsApp opt-out instruction or approved template on every WhatsApp step.
* **Suppression and frequency caps:** check the tenant's suppression list and frequency caps before sending. A fallback is still a new channel send and must be covered by the channel's consent and suppression settings.
* **Data minimization:** send only the contact and cart attributes needed to recover the cart. Keep payment details and sensitive identity data out of the flow context.

These are tenant-owned controls. Review [quiet hours configuration](/guides/quiet-hours-configuration), [message suppression](/guides/message-suppression), and [opt-out rules](/guides/opt-out-rules) before enabling a production flow.

## Troubleshooting

| Symptom | Check |
| - | - |
| The flow never starts | Confirm the event name exactly matches `cart.created`, the contact has an addressable channel, and the webhook or integration delivery is accepted. |
| The message sends after purchase | Compare the `cart_id` on `cart.created` and `order.paid`; a contact-only condition can match the wrong cart. |
| A product link is blank | Inspect the transform step input and confirm the source sent `recovery_url`; use a flat `{{cart_link}}` placeholder. |
| WhatsApp never receives the fallback | Check the SMS execution's terminal delivery status, WhatsApp consent, and the tenant's configured WhatsApp sender/template. |
| A customer receives duplicate reminders | Deduplicate source events by the store delivery ID and stable `cart_id` before enrollment. |

## Next steps

* [Flows overview](/flows/overview) — trigger and node semantics.
* [Flow Builder](/flows/builder) — build and test the graph visually.
* [Smart-send fallback chains](/guides/smart-send-fallback-chains) — configure channel fallback policy.
* [Shopify integration](/guides/shopify-integration) and [WooCommerce integration](/guides/woocommerce-integration) — connect the store and inspect event fields.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.