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_hereX-API-Key: ck_your_key_herealso 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=100import requests
response = requests.get(
"https://api.cartlytics.co/api/v1/public/carts",
headers={"Authorization": "Bearer ck_your_key_here"},
params={"modified_since": "2026-10-28T00:00:00Z", "limit": 100},
timeout=30,
)
response.raise_for_status()
for cart in response.json()["data"]:
print(cart["id"], cart["status"])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"import requests
response = requests.get(
"https://api.cartlytics.co/api/v1/public/carts/Z2NwLXVzLWVhc3QxOjAxSk",
headers={"Authorization": "Bearer ck_your_key_here"},
timeout=30,
)
response.raise_for_status()
cart = response.json()
print(cart["status"])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:00Zimport requests
response = requests.get(
"https://api.cartlytics.co/api/v1/public/deleted-carts",
headers={"Authorization": "Bearer ck_your_key_here"},
params={"deleted_since": "2026-10-28T00:00:00Z"},
timeout=30,
)
response.raise_for_status()
for deletion in response.json()["data"]:
print(deletion["id"], deletion["deletedAt"])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
- The first run reads every page of
/carts. - Each next run passes the newest
modifiedAtit has asmodified_since. Every change since comes back: new carts, products added or removed, a customer email from checkout, a cart that became an order. - Replace your copy of each returned cart, and drop the carts that
/deleted-cartsreturns for the same time. A deletion counts only when itsdeletedAtis at or after the cart'smodifiedAt. - 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. - 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" }.
| Status | Meaning |
|---|---|
200 | Success (data may be an empty array). |
400 | A parameter is not valid. The error message says which. |
401 | Missing, invalid, or revoked API key. |
403 | The store's plan does not include the API. It is part of the Growth and Scale plans. |
404 | No cart with that ID in this store. |
429 | Over 60 requests in a minute. Wait for Retry-After seconds. |
500 | Unexpected server error. |