A gift with purchase produces two kinds of cart line: the trigger line, the product the shopper bought that earned the gift, and the reward line, the gift itself. Both are ordinary cart lines. What tells them apart is their line item properties. This page is the reference for reading those properties, plus recipes you can paste into a theme.

How a trigger line is marked

A trigger line is a normal paid line the shopper added. CartSella tags it so the gift can be linked back to what earned it.
_trigger_offer_id is the reliable one. It is written when the line is added, and the gift sync engine also self-heals it on later passes: if a line qualifies for an active offer and is missing the tag, it gets patched in, no matter how the line reached the cart. _cs and _cs_role are only written on the add path, so a line the theme added without CartSella seeing the request can carry _trigger_offer_id and nothing else.
Spend-threshold offers (“spend $100, get a gift”) have no trigger product, so their gifts are not tied to any line. Do not expect _trigger_offer_id to exist for them.

How a reward line is marked

Every gift line carries these:
Test for a gift with _cs_role, not with a zero price. A gift line is added at its normal price and discounted to free by a Shopify Function at checkout, so on the cart page it can still show its full price depending on how the offer is configured. Conversely a paid line can be free for unrelated reasons.

Why _cs_gift_for exists

Shopify merges two cart lines into one when they have the same variant and identical properties. If one offer has two different trigger products in the cart and both earn a gift of the same variant, those gifts would collapse into a single line of quantity 2, sitting nowhere near either trigger. So CartSella tags each gift line with the variant id of the trigger product that earned it. Different tag, different properties, separate lines, each landing next to its own trigger. The tag is only added when it is needed. With a single trigger product, or a spend threshold, the gift line has no _cs_gift_for at all. This is deliberate: tagging the common case would churn every existing untagged gift line into a remove-and-re-add.

How many gifts

The main condition multiplies. Buy N of the trigger product for one gift, and 2N earns two, 3N earns three. With several trigger products required together, the offer takes the lowest complete set count across them. A minimum cart total is a gate, not a multiplier: it either lets the gift through or blocks it, unless the merchant explicitly turned on “repeat the gift for every multiple of this amount”. The total is then split round-robin across the trigger products present, which is where _cs_gift_for comes from.

Legacy property names

CartSella renamed its properties for merchant-facing tidiness. The old names are read forever, not for a migration window. A shopper who left a tab open before the rename keeps running the cached script and keeps writing the old names, so a live cart can contain either format at any time.
Any code you write that reads these must accept both spellings. Reading only _cs_role means a shopper on a stale tab gets a gift line your theme renders as an ordinary product, with a price and a delete button.
Every recipe below reads both.

The cart note attribute

Attribution for the order lives on the cart, not on a line: _cs_attr, a JSON array with short keys.
t is entity_type, i is entity_id, st is entity_subtype, s is source. The full key table is on Cart line properties. Long key names are accepted on read, so a parser should try t then entity_type.
When there is nothing to attribute, _cs_attr is written as the literal two-character string "[]", never as "". Shopify deletes a note attribute set to an empty string, so an empty write would be undone and rewritten on every cart mutation forever. If your code copies or rewrites cart.attributes, pass "[]" through unchanged.

Recipes

Detect a gift line and render it differently

Drop this into your cart line snippet, wherever your theme renders item. It hides the price, shows a badge, and replaces the quantity stepper with a static label.
snippets/cart-line-gift.liquid
The cs_subtype == 'gift_with_purchase' check narrows this to gift with purchase specifically. Drop it if you want the same treatment for every CartSella reward line, including bundle and pack-selector gifts.

Hide CartSella properties from the property loop

The single most common thing a theme needs. Every CartSella property starts with an underscore, and the standard Shopify convention is to skip those, so a well-behaved theme already hides them. Check yours:

Read gifts and their triggers in JavaScript

Fetches /cart.js, finds every gift, which offer produced it, and which line triggered it.
assets/my-cartsella-helpers.js

Re-run your code when the cart changes

CartSella fires these on both document and window after every mutation it makes.
cart:refresh and cart:change carry the same payload. Pick one.

Rules you must not break

The gift line stops being recognised. The sync engine no longer sees it as its own, so it will not reconcile it, and the discount Function at checkout will not match it either. The shopper is charged full price for a gift the storefront told them was free.This includes “cleaning up” properties in a cart form, rebuilding a line through /cart/add.js with your own property object, or filtering properties before a /cart/change.js patch. Hiding a property from the UI is fine. Removing it from the line is not.
CartSella computes the correct gift quantity from the cart on every change and reconciles the cart to that number. A line you add will be removed on the next pass, or worse, will collide with a line CartSella adds and leave the shopper with two gifts, only one of which the discount Function frees.If you want a gift added, configure the offer. If you want a paid product added, add it without CartSella properties and let the engine tag it if it qualifies.
/cart/change.js assumes quantity: 1 when the field is omitted. A patch that only means to change properties therefore silently deletes the shopper’s other units: a line of 3 comes back as a line of 1.
Use the line key, not its 1-based position. A properties patch makes Shopify drop and re-append the line, so positions shift after the very first write and a second patch lands on a different product.
CartSella intercepts cart mutations and clamps them. A quantity increase on any reward line is reverted to the line’s current quantity. A decrease or removal on a gift with purchase reward line is reverted too. Your remove button on such a line will appear to do nothing, by design: the engine reconciles the gift from the trigger, so the way to remove the gift is to remove the trigger.If a removal does reach Shopify through a path CartSella did not intercept, the offer id is recorded in the _cs_removed_gifts cart attribute so the sync does not immediately re-add the gift. That marker is cleared once the trigger goes away or the shopper adds the gift back.Reward lines of the other types (buy_more_save_more, fixed_bundle, pick_your_bundle, bundle_builder) are freely removable. That is deliberate: they have no reconciliation loop, so locking them would strand an orphaned free line forever once its trigger left the cart. Removing a bundle’s free line does mean the shopper loses the discount on the paid lines of that bundle too, because the Function no longer matches the set.
Reordering means removing and re-adding, which is a data-loss risk on the shopper’s own products and changes which lines a bundle Function matches. CartSella positions gift lines by controlling the order things are added, never by rewriting existing lines.If a gift is not visually next to its trigger in your theme, reorder in your rendering, not in the cart.

See also