Cartlytics

Carts API Reference

Last updated: November 3, 2026

Pull your Shopify carts into Power BI, a Python script or any tool: every cart with its products, customer and status, and only what changed since your last sync. The API is read-only and included in the Growth and Scale plans.

  • Base URL: https://api.cartlytics.co/api/v1/public
  • Format: JSON over HTTPS. Every time is UTC, for example 2026-10-31T04:12:09Z, with milliseconds when they are not zero.
  • Stability: fields may be added; existing fields are never renamed or removed.

Authentication

  • Create a key in the Cartlytics app: Settings -> API -> Generate key. The key starts with ck_ and is shown once. Regenerating or revoking it stops the old key at once.
  • Each key belongs to one store. With several stores, create a key in each and call the API once per store.
  • Send the key on every request:
    Authorization: Bearer ck_your_key_here
    X-API-Key: ck_your_key_here also works. Never put the key in the URL.

GET/carts

The store's carts, oldest change first. Follow nextCursor until it is null to read every page.

  • modified_sinceRFC 3339optional

    Carts that changed at or after this time. Default: 30 days ago, which is as far back as carts are kept.

  • modified_beforeRFC 3339optional

    Carts that changed before this time. Optional.

  • statusstringoptional

    One of active, abandoned, converted, draft_order. Optional.

  • limitintegeroptional

    1 to 250. Default: 100.

  • cursorstringoptional

    The nextCursor of the previous page, unchanged.

Returns: data, an array of up to limit carts in modifiedAt order, and nextCursor, or null on the last page.

curl -G https://api.cartlytics.co/api/v1/public/carts \
  -H "Authorization: Bearer ck_your_key_here" \
  -d modified_since=2026-10-28T00:00:00Z \
  -d limit=100

Response 200

{
  "data": [
    {
      "id": "Z2NwLXVzLWVhc3QxOjAxSk",
      "cartId": 1042,
      "shop": "your-store.myshopify.com",
      "status": "abandoned",
      "createdAt": "2026-10-27T09:00:00Z",
      "lastActivityAt": "2026-10-28T10:00:00Z",
      "modifiedAt": "2026-10-28T10:12:41.512Z",
      "convertedAt": null,
      "orderId": null,
      "checkoutStartedAt": "2026-10-28T10:00:00Z",
      "recoveryEmailSentAt": null,
      "totalValue": 64,
      "currency": "USD",
      "itemCount": 2,
      "trafficSource": "google",
      "country": "US",
      "customer": {
        "id": "7712345678016",
        "name": "Ana",
        "email": "ana@example.com",
        "isGuest": false
      },
      "lineItems": [
        {
          "productId": "8676897554624",
          "variantId": "48766764384448",
          "title": "Hoodie",
          "quantity": 2,
          "unitPrice": 32,
          "linePrice": 64,
          "addedAt": "2026-10-27T09:00:00Z"
        }
      ]
    }
  ],
  "nextCursor": "eyJ1IjoxNzYxNjQ2MzYxNTEyLCJpIjoiWjJOd0xYIn0"
}

The cart

  • idstring

    The Shopify cart token. Unique across stores: use it as the key.

  • cartIdinteger | null

    The short Cart ID your storefront shows to shoppers. Unique within one store.

  • shopstring

    The store's myshopify.com domain.

  • statusstring

    converted, draft_order, active (activity in the last 24 hours) or abandoned. Active and abandoned are worked out when you read the cart: a cart with no new activity turns abandoned without a change, so a synced copy must work it out again from lastActivityAt.

  • createdAtRFC 3339

    When the cart started.

  • lastActivityAtRFC 3339

    The shopper's last cart activity.

  • modifiedAtRFC 3339

    When anything on the cart last changed, the shopper's activity or not. The sync key.

  • convertedAtRFC 3339 | null

    When the cart became an order.

  • orderIdstring | null

    The Shopify order ID.

  • checkoutStartedAtRFC 3339 | null

    The first checkout.

  • recoveryEmailSentAtRFC 3339 | null

    When Cartlytics sent the recovery email.

  • totalValuenumber

    The cart total after discounts: the sum of linePrice.

  • currencystring | null

    The store's currency, for example USD.

  • itemCountinteger

    Units in the cart.

  • trafficSourcestring | null

    Where the visit came from.

  • countrystring | null

    Two-letter country code.

  • customerobject | null

    id (the Shopify customer ID, null for a guest), name, email, isGuest. null when the cart holds no customer data.

  • lineItemsarray

    The products in the cart.

A line item

  • productIdstring | null

    Shopify product ID.

  • variantIdstring | null

    Shopify variant ID.

  • titlestring

    Product title.

  • quantityinteger

    Units on the line.

  • unitPricenumber

    Price of one unit, before discounts.

  • linePricenumber

    Line total, after discounts.

  • addedAtRFC 3339

    When the line was added.

GET/carts/{id}

One cart, by its id (the cart token) or by its short cartId.

Returns: One cart object, or 404.

curl https://api.cartlytics.co/api/v1/public/carts/Z2NwLXVzLWVhc3QxOjAxSk \
  -H "Authorization: Bearer ck_your_key_here"

Response 200

{
  "id": "Z2NwLXVzLWVhc3QxOjAxSk",
  "cartId": 1042,
  "shop": "your-store.myshopify.com",
  "status": "abandoned",
  "createdAt": "2026-10-27T09:00:00Z",
  "lastActivityAt": "2026-10-28T10:00:00Z",
  "modifiedAt": "2026-10-28T10:12:41.512Z",
  "convertedAt": null,
  "orderId": null,
  "checkoutStartedAt": "2026-10-28T10:00:00Z",
  "recoveryEmailSentAt": null,
  "totalValue": 64,
  "currency": "USD",
  "itemCount": 2,
  "trafficSource": "google",
  "country": "US",
  "customer": {
    "id": "7712345678016",
    "name": "Ana",
    "email": "ana@example.com",
    "isGuest": false
  },
  "lineItems": [
    {
      "productId": "8676897554624",
      "variantId": "48766764384448",
      "title": "Hoodie",
      "quantity": 2,
      "unitPrice": 32,
      "linePrice": 64,
      "addedAt": "2026-10-27T09:00:00Z"
    }
  ]
}

GET/deleted-carts

The carts removed from the store's data since a time, for example a cart the merchant deleted or one merged into another.

  • deleted_sinceRFC 3339optional

    Carts deleted at or after this time. Default: 30 days ago, which is as far back as carts are kept.

  • deleted_beforeRFC 3339optional

    Carts deleted before this time. Optional.

  • limitintegeroptional

    1 to 250. Default: 100.

  • cursorstringoptional

    The nextCursor of the previous page, unchanged.

Returns: data, an array of deletions, and nextCursor.

curl -G https://api.cartlytics.co/api/v1/public/deleted-carts \
  -H "Authorization: Bearer ck_your_key_here" \
  -d deleted_since=2026-10-28T00:00:00Z

Response 200

{
  "data": [
    {
      "id": "Z2NwLXVzLWVhc3QxOjAxSk",
      "shop": "your-store.myshopify.com",
      "deletedAt": "2026-10-29T08:30:00Z"
    }
  ],
  "nextCursor": null
}

Carts are kept for 30 days after their last activity. A cart older than that leaves the API without appearing here, so keep your own copy as history.

Sync only what changed

  1. The first run reads every page of /carts.
  2. Each next run passes the newest modifiedAt it has as modified_since. Every change since comes back: new carts, products added or removed, a customer email from checkout, a cart that became an order.
  3. Replace your copy of each returned cart, and drop the carts that /deleted-carts returns for the same time. A deletion counts only when its deletedAt is at or after the cart's modifiedAt.
  4. A cart can come back more than once: changed at that exact millisecond, or changed again during a long run. Keep one row per id: the newest.
  5. A cart turns from active to abandoned when 24 hours pass with no activity, which is not a change. On each run, work out active or abandoned again from lastActivityAt.

Changes from the last 10 seconds wait for the next run, so a page never misses a change that is still being saved.

Example: Python to CSV for Power BI

Run it on a schedule. It keeps cartlytics_carts.csv up to date with one row per product in each cart.

import os
import time

import pandas as pd
import requests

BASE = "https://api.cartlytics.co/api/v1/public"
HEADERS = {"Authorization": "Bearer ck_YOUR_KEY"}
CSV = "cartlytics_carts.csv"


def fetch_all(path, params):
    """Follow nextCursor to the last page, and wait out a 429."""
    rows, cursor = [], None
    while True:
        query = dict(params, limit=250, **({"cursor": cursor} if cursor else {}))
        page = requests.get(f"{BASE}/{path}", headers=HEADERS, params=query, timeout=30)
        if page.status_code == 429:
            time.sleep(int(page.headers.get("Retry-After", "60")))
            continue
        page.raise_for_status()
        body = page.json()
        rows += body["data"]
        cursor = body["nextCursor"]
        if not cursor:
            return rows


COLUMNS = ["shop", "cart_id", "status", "modified_at", "last_activity", "cart_value",
           "customer_id", "customer_email", "variant_id", "product", "quantity", "line_price"]


def when(value):
    return pd.to_datetime(value, format="ISO8601", utc=True)


# Resume from the newest change already saved. The first run takes the last 30 days.
# dtype=str keeps the Shopify IDs as text, so a blank cell does not turn them into floats.
old = pd.read_csv(CSV, dtype=str) if os.path.exists(CSV) else pd.DataFrame(columns=COLUMNS)
since = {}
if not old.empty:
    since = {"modified_since": when(old["modified_at"]).max().strftime("%Y-%m-%dT%H:%M:%S.%fZ")}

changed = fetch_all("carts", since)
deleted = fetch_all("deleted-carts", {"deleted_since": since["modified_since"]} if since else {})

# A cart that changed during the run comes back twice: keep its newest version.
newest = {}
for cart in changed:
    seen = newest.get(cart["id"])
    if seen is None or when(cart["modifiedAt"]) > when(seen["modifiedAt"]):
        newest[cart["id"]] = cart

# Deletions come in time order, so the newest one per cart wins.
deleted_at = {d["id"]: when(d["deletedAt"]) for d in deleted}

rows = []
for cart in newest.values():
    # A deletion counts only when it came after the cart's last change.
    if cart["id"] in deleted_at and deleted_at[cart["id"]] >= when(cart["modifiedAt"]):
        continue
    customer = cart["customer"] or {}
    for line in cart["lineItems"] or [{}]:
        rows.append({
            "shop": cart["shop"], "cart_id": cart["id"], "status": cart["status"],
            "modified_at": cart["modifiedAt"], "last_activity": cart["lastActivityAt"],
            "cart_value": cart["totalValue"], "customer_id": customer.get("id"),
            "customer_email": customer.get("email"), "variant_id": line.get("variantId"),
            "product": line.get("title"), "quantity": line.get("quantity"),
            "line_price": line.get("linePrice"),
        })

# A changed cart replaces all of its old rows, and a deleted cart drops them.
old = old[~old["cart_id"].isin(set(newest) | set(deleted_at))]
out = pd.concat([old, pd.DataFrame(rows, columns=COLUMNS, dtype=object)], ignore_index=True)

# active and abandoned follow the clock, not a change, so work them out on every run.
recent = when(out["last_activity"]) > pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=24)
open_cart = ~out["status"].isin(["converted", "draft_order"])
out.loc[open_cart, "status"] = recent[open_cart].map({True: "active", False: "abandoned"})
out.to_csv(CSV, index=False)

Rate limit

60 requests per minute per store. Over that, the API answers 429 with a Retry-After header in seconds.

Errors

Every error body is { "error": "message" }.

StatusMeaning
200Success (data may be an empty array).
400A parameter is not valid. The error message says which.
401Missing, invalid, or revoked API key.
403The store's plan does not include the API. It is part of the Growth and Scale plans.
404No cart with that ID in this store.
429Over 60 requests in a minute. Wait for Retry-After seconds.
500Unexpected server error.