/checkout/buy-now
{% form 'buy_now' %}
Starts checkout for the submitted selection without adding to or replacing the shopping cart.
Form fields
-
product_idRequired Product to purchase.
-
variant_idOptional Required when the product has variants.
-
quantityOptional Defaults to 1.
-
properties[<namespace>.<key>]…Optional Declared buyer-writable cart-line properties.
-
gift_card_send_as_giftOptional Gift card products only. Send true to deliver the card to the recipient fields below. Leave it (and gift_card_recipient) out to deliver the card to the buyer's checkout contact.
-
gift_card_recipient_nameOptional Recipient name. Read only when gift_card_send_as_gift is true.
-
gift_card_recipient_emailOptional Recipient email. Sending as a gift needs an email or a phone; when both are present the card is sent by email.
-
gift_card_recipient_phoneOptional Recipient phone in international format, starting with +.
-
gift_card_recipient_messageOptional Optional message shown with the card.
-
gift_card_deliver_atOptional Optional future send time in RFC 3339 with an offset, for example 2026-12-24T09:00:00+05:30. A bare datetime-local value (2026-12-24T09:00) is read as UTC, so convert the shopper's local time before submitting. Leave it out to send right after payment.
-
gift_card_recipientOptional Alternative to the separate fields: one JSON object {name, email, phone, message, deliver_at}. Unknown keys are refused. When present it is used instead of the separate fields.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
Validation
Product, selected variant, quantity, inventory, sales-channel availability and buyer-writable properties follow the same validation as Add to Cart. Required app customizations cannot be bypassed. The checkout URL belongs to the current shopper; it is not a shareable sign-in link.
Limits
Quantity defaults to 1 and follows stock and purchase limits. Reuse a retry key only with the same selection. Starting checkout does not reserve stock or skip verification, delivery, payment or final availability checks.
Response
{
success,
checkout_url,
user_errors,
refresh_required: false
}
Open checkout_url in the hosted checkout popup. The original shopping cart and its discounts stay unchanged. The browser form response redirects to checkout. A conflicting retry key returns success: false with user_errors.
/cart/add
{% form 'cart_add' %}
quantity defaults to 1; variant_id is required for products with variants.
Form fields
-
product_idRequired Product to add.
-
purchase_intentOptional cart (default) adds to the shopping cart. buy_now opens checkout for this selection while preserving the shopping cart. A submit button may supply this value.
-
variant_idOptional Required when the product has variants.
-
quantityOptional Defaults to 1.
-
properties[<namespace>.<key>]…Optional Declared buyer-writable cart-line properties.
-
gift_card_send_as_giftOptional Gift card products only. Send true to deliver the card to the recipient fields below. Leave it (and gift_card_recipient) out to deliver the card to the buyer's checkout contact.
-
gift_card_recipient_nameOptional Recipient name. Read only when gift_card_send_as_gift is true.
-
gift_card_recipient_emailOptional Recipient email. Sending as a gift needs an email or a phone; when both are present the card is sent by email.
-
gift_card_recipient_phoneOptional Recipient phone in international format, starting with +.
-
gift_card_recipient_messageOptional Optional message shown with the card.
-
gift_card_deliver_atOptional Optional future send time in RFC 3339 with an offset, for example 2026-12-24T09:00:00+05:30. A bare datetime-local value (2026-12-24T09:00) is read as UTC, so convert the shopper's local time before submitting. Leave it out to send right after payment.
-
gift_card_recipientOptional Alternative to the separate fields: one JSON object {name, email, phone, message, deliver_at}. Unknown keys are refused. When present it is used instead of the separate fields.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Positive product/variant ids; variant must belong to the product; quantity, inventory, purchase state, channel availability, and declared buyer-writable properties are validated server-side. Gift card recipient fields are read only for products with is_gift_card: sending as a gift needs a valid email or a + international phone, and deliver_at must be in the future.
Limits
Quantity defaults to 1. The final line quantity cannot exceed available stock or product.quantity_max (or the selected variant's quantity_max). At most 20 properties per line.
Response
JSON returns {success, cart, user_errors, refresh_required}. cart is the committed compact cart snapshot; refresh the storefront context when refresh_required is true. A refused action returns the unchanged current cart.
/cart/clear
{% form 'cart_clear' %}
Empties the cart.
Form fields
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Requires a valid visitor/customer cart owner.
Limits
No action-specific numeric limit.
Response
{
success,
cart,
user_errors
}
/cart/fees
{% form 'cart_fee_select' %}
Adds or removes an optional checkout fee (for example gift wrapping) from ctx.cart.optional_fees. Checkout starts with these fees selected, prices and taxes them, and the shopper can still change them there. Example: {% form 'cart_fee_select' %}{% endform %}
Form fields
-
fee_idRequired id of an entry in ctx.cart.optional_fees.
-
selectedRequired true to add the fee to the order, false to remove it.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
Validation
fee_id must be an enabled optional fee of the cart's current market, as listed in ctx.cart.optional_fees; anything else (an unknown, disabled, required or other market's fee) returns CART_OPTIONAL_FEE_INVALID and leaves the cart unchanged. selected must be true or false. Removing a fee the cart carries is always allowed. Selections are kept with the market they were made in: after the shopper changes country they no longer apply and the next selection replaces them.
Limits
At most 10 selected fees (CART_OPTIONAL_FEE_LIMIT). Repeating the same selection changes nothing. The cart does not price or tax fees; checkout computes and charges them for the delivery market, which may differ from the cart's.
Response
{
success,
cart,
user_errors,
refresh_required
}
, like the other cart actions; read the new selection from ctx.cart.optional_fees after refreshing. Example: sending fee_id=8d1c…&selected=true&_format=json to /cart/fees returns {"success": true, "cart": {…}, "user_errors": [], "refresh_required": true}. A form submission redirects back with HTTP 303.
/cart/note
{% form 'cart_note_update' %}
Saves a note for the store (for example delivery instructions). Checkout shows it and places it on the order as the customer note. Example: {% form 'cart_note_update' %}{% endform %}
Form fields
-
noteRequired The note for the store. Empty clears the note. At most ctx.cart.note_max_length characters.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
Validation
The note is trimmed; line breaks and tabs are kept and other control characters are removed. An empty note clears it.
Limits
At most ctx.cart.note_max_length characters (2000 by default, the order customer-note limit), counted as Unicode code points, so a letter in any script counts once. A longer note returns CART_NOTE_TOO_LONG and leaves the saved note unchanged.
Response
{
success,
cart,
user_errors,
refresh_required
}
, like the other cart actions; read the saved note from ctx.cart.note after refreshing. Example: sending note=Please+leave+at+the+door&_format=json to /cart/note returns {"success": true, "cart": {…}, "user_errors": [], "refresh_required": true}. A form submission redirects back with HTTP 303.
/cart/notify-me
{% form 'cart_notify_me' %}
Back-in-stock subscription; guests supply contact details, customers default to their account email.
Form fields
-
product_idRequired Out-of-stock product to watch.
-
variant_idOptional Specific variant to watch.
-
contact_typeOptional email or sms; defaults to email.
-
emailOptional Required for guests using email contact.
-
phoneOptional Required when contact_type is sms.
Validation
Positive product/variant ids; the variant must belong to the product, which must be published to the current channel and either scheduled or out of stock. Restock alerts must be enabled. Guests may subscribe. contact_type is email (default) or sms. An existing account email/phone is used; customers without one enter that contact. SMS requires enabled restock SMS and a ready SMS provider, and stores a valid E.164 number.
Limits
Email is limited to 256 characters. Requests expire after the merchant-configured 1–365 day window. Repeated pending requests for the same store, product, variant and contact refresh the request. A queued request is returned unchanged; it does not create another delivery.
Response
{
success,
response,
user_errors
}
success confirms the request was saved, not that a message was sent. The bridge wrapper is tringify.cart.notifyMe({product_id, variant_id?, contact_type?, email?, phone?}); it includes the CSRF token and returns this same envelope.
/cart/remove
{% form 'cart_remove' %}
Removes the product (or specific variant) line.
Form fields
-
product_idRequired Cart line's product.
-
variant_idOptional Cart line's variant, when the product has variants.
-
line_idOptional Required to target a line carrying properties.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Positive product/variant ids; line_id targets one exact line. Bare product/variant targeting only matches a property-less line.
Limits
No action-specific numeric limit.
Response
{
success,
cart,
user_errors
}
/cart/update
{% form 'cart_update' %}
Sets the line to quantity; 0 removes it.
Form fields
-
product_idRequired Cart line's product.
-
variant_idOptional Cart line's variant, when the product has variants.
-
quantityRequired New quantity; 0 removes the line.
-
line_idOptional Required to target a line carrying properties.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Positive product/variant ids and integer quantity; quantity 0 removes. line_id is required for a line with properties and cannot select another visitor's line.
Limits
The final line quantity cannot exceed the current cart line's quantity plus quantity_can_add. Available inventory and the resolved variant → product → store order cap are enforced.
Response
{
success,
cart,
user_errors
}
cart is the current enriched cart and is unchanged when refused.
/cart/discount/apply
{% form 'discount_code_apply' %}
Validates the code against the current cart, customer, market, and stacking rules, then stores it only when it can apply. Unknown or ineligible codes leave the cart unchanged.
Form fields
-
codeRequired The discount code to apply.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Non-empty code; the same store-scoped pricing evaluator used to advertise available discounts validates code existence, activity, eligibility, stacking, market, customer, and current cart conditions before the cart changes.
Limits
Applied-code count and discount limits come from order-pricing configuration. Codes that do not apply are limited to 10 per visitor session and 30 per address in 15 minutes; further attempts return CART_COUPON_ATTEMPTS_LIMITED (HTTP 429) until the window ends. Codes that apply are not counted.
Response
{
success,
cart,
user_errors
}
ineligible codes return the unchanged current cart.
/cart/discount/remove
{% form 'discount_code_remove' %}
Removes the applied code (no-op when absent).
Form fields
-
codeRequired The applied code to remove.
-
_formatOptional Set to json when submitting with fetch; omit for the normal browser redirect.
-
client_mutation_idOptional Unique retry key, up to 128 characters. Reusing it for the same action prevents a successful mutation from being applied twice for 24 hours.
Validation
Non-empty code; removing a code that is not applied is an idempotent no-op.
Limits
No action-specific numeric limit.
Response
{
success,
cart,
user_errors
}
User error codes#
Code |
When |
|---|---|
STOREFRONT_CART_PRODUCT_INVALID |
product_id missing or not purchasable. |
STOREFRONT_CART_VARIANT_INVALID |
variant_id missing for a variable product, or wrong. |
STOREFRONT_CART_OWNER_REQUIRED |
No usable visitor session — reload the page. |
Quantity, stock, and purchase-state validation failures come back the same way — as user_errors with the CURRENT unchanged cart, never as a broken state. After a successful action, ctx.cart reflects the change on the next render.
Line properties#
A cart line can carry declared properties — the personalization channel (engraving text, a gift note). Every property key must match one of the store's cart line definitions (Settings → Metafield definitions → Cart lines, or an app's declaration), written as "
{% form 'cart_add' %}
<input type="hidden" name="product_id" value="{{ product.id }}">
<input name="properties[custom.engraving]" maxlength="30" placeholder="{{ 'products.engraving' | t }}">
<button type="submit">{{ 'products.add_to_cart' | t }}</button>
{% endform %}
Properties are part of LINE IDENTITY: the same product with different property values is a SEPARATE cart line (two mugs, two engravings), each with its own line_id. Cart line forms (quantity steppers, remove buttons) must post the line's line_id — bare product/variant targeting matches only the property-less line, never silently one of several personalized lines.
{% for line in cart.items %}
{% for prop in line.properties %}<p>{{ prop[1] }}</p>{% endfor %}
{% form 'cart_remove' %}
<input type="hidden" name="product_id" value="{{ line.product_id }}">
<input type="hidden" name="line_id" value="{{ line.line_id }}">
<button type="submit">{{ 'cart.remove' | t }}</button>
{% endform %}
{% endfor %}
Code |
When |
|---|---|
CART_PROPERTY_UNKNOWN |
The key matches no cart line definition on this store. |
CART_PROPERTY_INVALID |
The value fails the definition's type or rules. |
CART_PROPERTY_NOT_WRITABLE |
The definition is not buyer-writable (hidden), or an app wrote outside its namespace. |
CART_PROPERTY_TOO_MANY |
More properties than one line allows. |