# Getting Started (/docs)
Next Commerce is an ecommerce platform for DTC merchants. It includes a full storefront, checkout, payments, and order management — plus a headless campaigns layer for building custom checkout funnels. Use this guide to find the right starting point for what you're building.
## Platform Overview [#platform-overview]
Campaigns and storefronts are the customer-facing layer — both produce orders in your Next Commerce store. The Admin API gives you programmatic access to all store data. Apps are how you package and distribute integrations across multiple stores.
Already decided to start a new Campaign Page Kit project? The [**Campaign agent quickstart**](/docs/agent-setup) gives your coding agent a thin wrapper around the current CLI while preserving your template, route, and project choices. For reusable guidance across platform tasks, use [**Next Commerce AI Skills**](/docs/skills).
Before you ship, run your flows through [**Testing**](/docs/testing) — safe QA on a live store with no real charges.
## What Are You Building? [#what-are-you-building]
***
### Build a Campaign Funnel [#build-a-campaign-funnel]
Use campaigns to build external checkout experiences that run outside the storefront — landing pages, checkout, upsell flows, and receipt pages.
**Choose your approach**
| Approach | Best for | Description |
| -------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------- |
| [Campaign Cart SDK](https://cart-sdk.nextcommerce.com/latest/) | Fastest path | HTML/JS SDK using `data-next-*` attributes — no custom JavaScript required |
| [Campaign Cart API](/docs/campaigns/api) | Full control | Headless JS API for custom checkout implementations |
**Get started**
1. In your dashboard, install the **Campaigns App** and create a campaign
2. Add packages linked to product variants in your catalog
3. Get your **API Key** from the campaign's **Integration** tab
4. Clone the [Campaign Cart Starter Template](https://github.com/NextCommerceCo/campaign-cart-example) — a pre-built landing page, checkout, upsell, and receipt flow ready to customize
**Resources**
* [Campaigns overview](/docs/campaigns) — campaign structure and funnel flow
* [Campaign Cart SDK](https://cart-sdk.nextcommerce.com/latest/) — data attributes, JavaScript API, and analytics events
* [Campaign Cart API](/docs/campaigns/api) — REST API reference
***
### Customize a Storefront Theme [#customize-a-storefront-theme]
Use themes to control the appearance and behavior of your storefront — product pages, catalog, cart, and storefront checkout flow. Themes use HTML, CSS, JavaScript, and a liquid-like template language with access to storefront objects and a GraphQL API.
**Get started**
1. Install [Theme Kit](/docs/storefront/themes/theme-kit), the CLI for local theme development
2. Start from [Spark](https://github.com/NextCommerceCo/spark), the Tailwind CSS starter theme
3. Run `ntk pull` to sync files locally, make changes, then `ntk push` to deploy
**Resources**
* [Themes overview](/docs/storefront/themes) — theme structure and Theme Kit
* [Template reference](/docs/storefront/themes/templates) — tags, objects, filters, and URL routing
* [Theme guides](/docs/storefront/themes/guides/custom-page-templates) — custom page templates, product templates, and variants
* [Storefront GraphQL API](/docs/storefront/graphql) — fetch products, cart data, and more
* [Event tracking](/docs/storefront/event-tracking) — track storefront events and conversions
***
### Integrate with the Admin API [#integrate-with-the-admin-api]
Use the Admin API to manage store data and build backend integrations — orders, subscriptions, products, customers, and fulfillment. All requests authenticate with OAuth 2.
**Get started**
1. In your dashboard, go to **Settings > API Access** and create an OAuth App
2. Select the [permissions](/docs/admin-api/permissions) your integration needs to get an Access Token
**Resources**
* [Admin API overview](/docs/admin-api) — authentication, versioning, and rate limits
* [API reference](/docs/admin-api/reference/orders/ordersCreate) — full endpoint reference
* [Order management](/docs/admin-api/guides/order-management) — create and manage orders
* [Subscription management](/docs/admin-api/guides/subscription-management) — recurring billing
* [Payment integrations](/docs/admin-api/guides/external-checkout) — Apple Pay, Google Pay, PayPal, Klarna, and more
***
### Build an App [#build-an-app]
Use apps to package an integration as an installable unit that works across multiple stores. An app can combine the Admin API, Webhooks, and storefront extensions, with an OAuth-based install flow that merchants complete in one click.
**When to build an app vs. a direct integration**
* You're distributing your integration to multiple merchants
* Your integration spans both backend logic (Admin API, Webhooks) and storefront UI (snippets, event tracking)
* You need per-store OAuth tokens that merchants can grant and revoke
**Get started**
1. Review the [example apps](#example-apps) to understand the full pattern before writing any code
2. Define your [App Manifest](/docs/apps/manifest)
3. Implement the [OAuth install flow](/docs/apps/oauth/getting-started)
**Example apps**
| App | What it covers |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [Example S2S App](https://github.com/NextCommerceCo/example-app) | OAuth flow, session tokens, remote settings, webhooks |
| [Google Analytics 4](https://github.com/NextCommerceCo/google-analytics-4) | Snippets and event tracking in storefronts |
| [Fulfillment Service](https://github.com/NextCommerceCo/demo-fulfillment-service-app) | OAuth flow and fulfillment API integration |
**Resources**
* [Apps overview](/docs/apps)
* [OAuth & install flows](/docs/apps/oauth)
* [Server-to-server guide](/docs/apps/guides/server-to-server-apps)
* [Storefront extension guide](/docs/apps/guides/storefront-extension)
* [Webhooks](/docs/webhooks)
# Admin API (/docs/admin-api)
### Getting Started [#getting-started]
At the core of Next Commerce, the Admin API lets developers manage store resources, integrate third-party services, and build seamless external order flows.
### Authentication [#authentication]
The Admin API uses the OAuth 2 authorization protocol to manage access to your store's resources. OAuth apps and associated access tokens can be tailored with object-level permissions to ensure that each integrated service only has access to the objects it needs.
Before using the Admin API, you'll need to create a store and an OAuth app for API access. To create an OAuth app, navigate to **Settings > API Access** and create a new OAuth app with the applicable [permissions](/docs/admin-api/permissions) to retrieve your **Access Token**. It is recommended to create unique OAuth apps for each external system so you can revoke access as needed.
```bash title="Admin API Path"
https://{store}.29next.store/api/admin/
```
Next Commerce was formerly 29 Next, and the platform still carries that name in its core technical identifiers: store and account hostnames (`{store}.29next.store`, `accounts.29next.com`), the `X-29next-API-Version` and `X-29Next-Signature` headers, and the API key namespace. These are current, in use on every store, and not scheduled to change. Use them exactly as written.
**Use your OAuth app access token in the request headers to access the API.**
```bash title="Example Request"
curl -X GET "https://{store}.29next.store/api/admin/" \
-H "Authorization: Bearer " \
-H "X-29next-API-Version: 2024-04-01"
```
Admin API tokens provide full access to your system, including the ability to perform destructive actions like deleting data or users. These tokens should never be shared publicly or exposed in client-side code.
**Always keep your Admin API tokens private and secure.**
### Versioning [#versioning]
API versioning allows Next Commerce to continuously evolve the platform while maintaining predictable behavior for existing APIs with a path for upgrades and deprecations.
**Admin API Versions**
| Version | Status | Docs |
| ------------ | -------------------- | ------------------------------------------------------------------------ |
| `2023-02-10` | Deprecated (legacy) | [View Reference](/docs/admin-api/reference/2023-02-10/orders/ordersList) |
| `2024-04-01` | Stable (recommended) | [View Reference](/docs/admin-api/reference/orders/ordersList) |
| `unstable` | Unstable | [View Reference](/docs/admin-api/reference/unstable/orders/ordersList) |
**Specify an API Version**
To specify a version, pass the `X-29next-API-Version` header with your desired API version.
It is **highly recommended** to specify your version on your API requests to ensure consistency for your integration.
### Rate Limits [#rate-limits]
Admin APIs are rate-limited to maintain the stability and equity of our platform for all users. We employ a number of methods to enforce rate limits.
| API | Rate Limit Method | Limit |
| --------- | ----------------- | ----------------- |
| Admin API | Request-based | 4 requests/second |
Once you reach API rate limits you'll then receive a 429 Too Many Requests response, and a message that a throttle has been applied.
We recommend that API users limit calls appropriately, cache results, and retry requests using industry best practices to avoid rate-limit errors.
# Permissions (/docs/admin-api/permissions)
Admin API access is controlled granularly by Scopes that are associated with each OAuth App and associated Access Tokens.
| Scope | Detail |
| --------------------------- | ------------------------------------------------------------------------- |
| `admin:read` | Access to list and view all data |
| `admin:write` | Access to create and update all data |
| `campaigns:read` | Access to list and view campaigns |
| `campaigns:write` | Access to create and update campaigns |
| `carts:read` | Access to list and view carts |
| `carts:write` | Access to create and update carts |
| `catalogue:read` | Access to list and view catalogue objects such as products and categories |
| `catalogue:write` | Access to create and update catalogue related objects |
| `content:read` | Access to list and view storefront content related objects |
| `content:write` | Access to create and update storefront content related objects |
| `disputes:read` | Access to list and view disputes |
| `disputes:write` | Access to create and update disputes |
| `exports:read` | Access to list and view all exports |
| `exports:write` | Access to create and update exports |
| `fulfillment_service:read` | Access to list assigned fulfillment orders and own locations |
| `fulfillment_service:write` | Access to create fulfillment locations and fulfillments |
| `fulfillment_orders:read` | Access to list fulfillment order fulfillment requests |
| `fulfillment_orders:write` | Access to update fulfillment order fulfillment requests |
| `gateways:read` | Access to list and view all gateways and gateway groups |
| `gift_cards:read` | Access to list and view all gift cards |
| `gift_cards:write` | Access to create and update gift cards |
| `locations:read` | Access to list and view all locations |
| `locations:write` | Access to create and update locations |
| `metadata:read` | Access to list and view all metadata definitions |
| `metadata:write` | Access to create and update metadata definitions |
| `orders:read` | Access to list and view all orders |
| `orders:write` | Access to create and update orders |
| `store:read` | Access to list and view store |
| `subscriptions:read` | Access to list and view all subscriptions |
| `subscriptions:write` | Access to create and update subscriptions |
| `themes:read` | Access to list and view all themes |
| `themes:write` | Access to create and update themes |
| `transactions:read` | Access to list and view all transactions |
| `transactions:write` | Access to create and update transactions |
| `tickets:read` | Access to list and view all tickets |
| `tickets:write` | Access to create and update tickets |
| `users:read` | Access to list and view all users |
| `users:write` | Access to create and update users |
| `webhooks:read` | Access to list and view all webhooks |
| `webhooks:write` | Access to create and update webhooks |
# Campaign agent quickstart (/docs/agent-setup)
Give your coding agent this instruction:
```text
Read https://developers.nextcommerce.com/agent-setup/prompt.md and use it to start a new Campaign Page Kit project for this task. Preserve my existing choices. If the template, route slug, campaign name, or agent-context preference is missing, ask me before creating files.
```
The fetched file is plain Markdown at a stable URL. It uses the same source as the instructions below, contains no credentials, and keeps the current CLI and Campaigns docs authoritative. It is a campaign-specific quickstart, not general setup for every kind of Next Commerce development.
## Instructions your agent receives [#instructions-your-agent-receives]
Use this quickstart only when the user wants a new Campaign Page Kit workspace. It is an optional agent-friendly wrapper around the current Campaigns documentation and `campaign-init`; it is not a general Next Commerce development environment, a production architecture decision, or a replacement for the developer's chosen workflow.
If the user wants to understand the platform, work in an existing project, build a storefront theme or app, or use the Admin API, do not scaffold a campaign. Read the [developer index](https://developers.nextcommerce.com/llms.txt?ref=agent-setup) and [platform capability map](https://developers.nextcommerce.com/capabilities.json?ref=agent-setup), identify the relevant documentation, and respond to that task instead.
## Before writing [#before-writing]
* Confirm that the user's request calls for a new Campaign Page Kit project. If it is unclear, ask one concise question before installing packages or creating files.
* Inspect the current directory. If it contains an existing project, do not add a second project or replace its configuration. Ask whether to use that project or create a new subdirectory.
* Use the Node.js version required by the current [Campaigns quick start](https://developers.nextcommerce.com/docs/campaigns?ref=agent-setup). Do not impose a stricter version or relax the documented requirement.
* Preserve any template, source, project name, route slug, package manager, and agent-context choice the user has already made. The commands below use the docs' npm path for an empty project; do not replace an existing project's package-manager convention.
* Do not request, print, copy, or invent credentials. A workspace can be scaffolded and built without a Campaign API key.
* Do not call a live store or place an order unless the user has identified the store, authorized the action, and provided suitable test access.
## Use the current Campaign Page Kit workflow [#use-the-current-campaign-page-kit-workflow]
Read the [Campaign Page Kit guide](https://developers.nextcommerce.com/docs/campaigns/page-kit?ref=agent-setup), then inspect the installed CLI contract before constructing a command:
```bash
npm init -y
npm install next-campaign-page-kit
npx campaign-init --help
```
If the user has not supplied a template, route slug, campaign name, and agent-context preference, ask for the missing choices together. Do not silently select a starter template or context file. Use the current template picker or catalog described by the CLI and docs rather than copying a fixed list from this prompt.
For a non-interactive agent run, replace every placeholder below with the developer-confirmed value:
```bash
npx campaign-init --non-interactive --json \
--template \
--slug \
--name "" \
--ai-context
```
Choose the context value that matches the current tool, or `none` when the tool is unsupported or the developer does not want a generated context file:
| Tool | `--ai-context` value |
| -------------- | -------------------- |
| Claude Code | `claude` |
| OpenAI Codex | `codex` |
| Cursor | `cursor` |
| GitHub Copilot | `copilot` |
| Other agents | `none` |
`campaign-init` is the authority for scaffold behavior, supported flags, conflict handling, and exit codes. Do not reproduce the scaffold manually if it fails. Preserve its output and follow the troubleshooting guidance in the Campaign Page Kit guide.
The [Next Commerce AI skills guide](https://developers.nextcommerce.com/docs/skills?ref=agent-setup) describes optional reusable skills. Installing `next-campaigns-setup` is not required for this quickstart. Use it only when the developer asks for the fuller guided setup/configuration workflow or another official tool routes the task to it.
## Verify only what was requested [#verify-only-what-was-requested]
Run the static build:
```bash
npm run build
```
Use the JSON emitted by `campaign-init` and the generated project files to confirm that:
* the selected campaign is registered in `_data/campaigns.json`;
* its source directory exists under `src/`;
* the requested agent context file was created, or no context file was requested;
* the build exits successfully and writes the campaign to `_site/`.
Inspect one generated entry page and briefly describe what the selected starter contains. Treat that as proof of a local scaffold only. Do not claim the project is connected to a store, production-ready, or tested end to end without the corresponding evidence.
If the user explicitly wants to continue, follow the current Campaigns and testing documentation for configuration and test-store work. Otherwise stop after the verified local build; do not turn a quickstart into an unsolicited production setup.
## Report back [#report-back]
Tell the user:
1. Which directory and existing/new project path you used.
2. Which developer-provided template, slug, name, and agent-context value you used.
3. Which official docs and CLI contract you followed.
4. Whether `campaign-init` and the static build passed.
5. What you verified in the generated output.
6. What remains unconfigured or untested, and the next step relevant to the user's stated goal.
# Getting Started (/docs/campaigns)
Campaigns are fully custom checkout funnels — landing, checkout, upsell, and receipt pages — backed by a CORS-enabled API that handles products, pricing, payments, and order creation. **No backend integration required.**
Orders created through a campaign are regular store orders. They appear in the store's Orders list alongside storefront orders and are available through the Admin API and webhooks like any other order.
The fastest path from zero to a working funnel on localhost is the **[Campaign Page Kit](https://github.com/NextCommerceCo/campaign-page-kit)**: a CLI that scaffolds an SDK-ready starter template, runs a hot-reload dev server, and outputs a static site you can deploy to Netlify, Cloudflare Pages, Vercel, or any static host.
## Quick Start [#quick-start]
### 1. Prerequisites [#1-prerequisites]
* **Node.js 18+**
* A **Next Commerce store** with a campaign created in the Campaigns App
* Your **Campaign API key** — find it under your campaign's *Integration* tab
### 2. Initialize a new project [#2-initialize-a-new-project]
In a fresh directory:
```bash
mkdir my-campaigns && cd my-campaigns
npm init -y
npm install next-campaign-page-kit
npx campaign-init
```
`campaign-init` walks you through everything in one prompt-driven flow:
1. Pick a starter template (see [Starter templates](#starter-templates) below)
2. Enter your campaign **name** and **slug** (the URL path, e.g. `/my-campaign/`)
3. The template is downloaded into `src//` and registered in `_data/campaigns.json`
4. CLI scripts (`dev`, `build`, `clone`, `config`, `compress`) are added to your `package.json`
5. Optionally, your **API key** is written to `assets/config.js`
6. Optionally, an **AI context** doc is installed for Claude Code, Cursor, Codex, or Copilot
The `--ai-context` step writes the right context file to the right path for your assistant — `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/`, or `.github/copilot-instructions.md`. Your AI editor will know the project structure, Liquid filters, and SDK conventions immediately.
Skipping the API key during init is fine — run `npm run config` later to set it.
### 3. Start the dev server [#3-start-the-dev-server]
```bash
npm run dev
```
This opens your campaign's first page (e.g. `//presell/`) in the browser and hot-reloads as you edit. If you have multiple campaigns in the project, you'll get a picker.
You now have a **complete campaign funnel** running on your local machine — presell, landing, checkout, upsells, and receipt — all wired up to your live store data through the SDK. Click through it and create a test order using the test card below.
### 4. Place a test order [#4-place-a-test-order]
Run the full funnel end-to-end on your local dev server. At checkout, pay with the test card below — it processes without a real transaction so you can verify presell → checkout → upsell(s) → receipt all wire up correctly.
| Card number | Expiry | CVV | What it does |
| --------------------- | --------------- | --- | --------------------------------------------- |
| `6011 1111 1111 1117` | Any future date | Any | Test payment success flow without transaction |
See [Testing](/docs/testing) for the full picture — 3DS, decline, and subscription test cards, the Test Gateway, and safe QA on live stores.
## What you got [#what-you-got]
Each campaign is fully isolated — its own layouts, assets, and config — so you can run multiple campaigns in one repo without them stepping on each other.
```
my-campaigns/
├── _data/
│ └── campaigns.json # Registry — all campaigns live here
├── src/
│ └── / # Your campaign
│ ├── _layouts/base.html # Page wrapper
│ ├── _includes/ # Reusable components
│ ├── assets/
│ │ ├── css/
│ │ ├── images/
│ │ ├── js/
│ │ └── config.js # SDK config — API key lives here
│ ├── presell.html
│ ├── checkout.html
│ ├── upsell.html
│ └── receipt.html
└── package.json
```
## Starter templates [#starter-templates]
Pick one at the `campaign-init` template prompt. Each template ships with SDK-ready checkout, upsell, and receipt surfaces, plus the supporting presell and landing pages for that family. Start from a maintained template, then replace campaign-specific copy, assets, package IDs, offer codes, shipping methods, tracking, and routes with values from Campaigns App and your build brief. The two Olympus templates are a good starting point for your first campaign.
[**See all available templates →**](/docs/campaigns/templates)
## Campaign Flow [#campaign-flow]
A typical campaign funnel guides customers through a series of pages:
* **Presell** — advertorial article that warms up cold traffic before the offer
* **Landing** — marketing page that drives traffic
* **Checkout** — package selection, shipping, payment
* **Upsells** — post-purchase offers
* **Receipt** — order confirmation
You can customize this flow to fit your campaign — add/remove pages, change the order, or even have multiple funnels for different audiences, customize the `next_page` parameter on each page that links pages to the next.
***
## Cart SDK [#cart-sdk]
The pages above are wired up by the **Campaign Cart SDK**. Its documentation is generated from the SDK source and versioned with each release, so it lives on its own site rather than here.
**[cart-sdk.nextcommerce.com →](https://cart-sdk.nextcommerce.com/latest/)**
| Reference | What it covers |
| ---------------------------------------------------------------------------------------- | ----------------------------------------- |
| [Getting started](https://cart-sdk.nextcommerce.com/latest/start-here/getting-started/) | Loading the SDK and configuring it |
| [How it works](https://cart-sdk.nextcommerce.com/latest/start-here/how-it-works/) | Boot sequence and progressive enhancement |
| [Data attributes](https://cart-sdk.nextcommerce.com/latest/reference/data-attributes/) | Every `data-next-*` attribute |
| [JavaScript API](https://cart-sdk.nextcommerce.com/latest/reference/javascript-api/) | The `window.next` methods |
| [Analytics events](https://cart-sdk.nextcommerce.com/latest/reference/analytics-events/) | Every `dl_*` event and its payload |
| [URL parameters](https://cart-sdk.nextcommerce.com/latest/reference/url-parameters/) | Parameters the SDK reads at boot |
| [Window globals](https://cart-sdk.nextcommerce.com/latest/reference/window-globals/) | Configuration on `window` |
| [Debugger](https://cart-sdk.nextcommerce.com/latest/reference/debugger/) | The on-page debug overlay |
Building a specific funnel page:
| Page | What it covers |
| -------------------------------------------------------------------------------------- | -------------------------------------- |
| [Landing and presell](https://cart-sdk.nextcommerce.com/latest/pages/landing-presell/) | Selectors, bundles, add to cart |
| [Checkout page](https://cart-sdk.nextcommerce.com/latest/pages/checkout-page/) | Form fields, payment, express checkout |
| [Upsell page](https://cart-sdk.nextcommerce.com/latest/pages/upsell-page/) | Accept and decline flows |
| [Receipt page](https://cart-sdk.nextcommerce.com/latest/pages/receipt-page/) | Order data rendering |
Use the version selector on that site to read the docs for the SDK version your campaign loads.
***
## Concepts [#concepts]
### Campaigns [#campaigns]
A **campaign** bundles everything needed for a checkout: packages, offers, shipping options, payment methods, and localization. Each campaign has a unique **API key** used by the SDK to authenticate requests.
### Packages [#packages]
A **package** is the campaign's sellable reference to a product or product variant. In SDK 0.4.x-compatible builds, keep packages focused on identity and base price:
* Create one package for the product or variant customers can buy.
* Use the package's base list price as the anchor price.
* Send the selected package ID and quantity from the page.
* Use **offers** to discount checkout quantity tiers and **Code** offers for upsells, downsells, and exit-pop incentives.
For bundle-based quantity discounts (e.g. "buy 3 for $21" or "buy 5 for $30"), use **offers** to discount the price at checkout rather than baking the discount into separate package records. This keeps the package list stable and lets the API return before/after pricing so template components can show savings consistently.
Package-level Quantity and Retail Price are legacy compatibility fields in Campaigns App. They may still appear when the campaign setting **Enable Package Retail Price & Quantity** is enabled, but new builds should prefer Offer-based price mutation.
Packages can also be **recurring** for subscription products. When you reference a package in HTML (`data-next-package-id="…"`), the ID comes from your campaign's package list in the Campaigns App.
### Offers [#offers]
**Offers** are price mutation rules that apply when an order meets a condition. Two flavours:
* **Offer** — automatic, applies when the cart matches the rule (checkout page only)
* **Code** — voucher-based, applies when the customer enters a code. Codes stack **on top of** automatic offers, and codes are also the mechanism for applying discounts on **upsell pages** (where automatic offers don't run).
The API returns adjusted before/after pricing so you can show savings. In modern Campaigns App setup, this replaces package-level compare-at pricing for most campaigns.
Every offer has a **condition** that decides when it applies and a **benefit** that decides what it discounts.
#### Condition types [#condition-types]
| Type | Applies when | Example |
| ------- | --------------------------------------------------------- | ---------------------------------- |
| `any` | Always. No quantity requirement. | A code that takes 10% off any cart |
| `count` | The quantity of a matching package is at or above `value` | Buy 2 or more Widgets |
A condition targets either every package on the campaign (`all_packages: true`, which also covers packages added later) or a specific list of `package_ids`.
#### Benefit types [#benefit-types]
All benefits are percentage based. `value` is the percentage off.
| Type | Discounts | Example |
| --------------------- | ------------------------------------------------------- | ----------------------------------------- |
| `package_percentage` | The unit price of the packages matched by the condition | 30% off each Widget when buying 2 or more |
| `shipping_percentage` | The shipping price | 50% off shipping when buying 3 or more |
| `order_percentage` | The entire order | 10% off everything with a welcome code |
A benefit can also set `price_rounding` so each discounted unit price ends in `.00`, `.95`, `.97`, or `.99`. Without it, the discounted price is not rounded.
Create and manage offers with the [Campaigns Admin API](/docs/campaigns/admin-api#add-offers-to-a-campaign). The Cart API returns the same condition and benefit fields on each offer from [campaignRetrieve](/docs/campaigns/api/campaigns/campaignRetrieve).
**Example — quantity-based bundle pricing:**
A single Widget package priced at $49.95/unit. Create one offer per quantity tier — including 1x — to control the discounted price the customer pays at each quantity:
| Quantity | List price | Offer | Customer pays | Savings |
| -------- | ---------- | ------------------------------ | ------------- | ------- |
| **1x** | $49.95 | Buy 1 → $39.95 (Save 20%) | $39.95 | $10.00 |
| **2x** | $99.90 | Buy 2 → $34.95 each (Save 30%) | $69.90 | $30.00 |
| **3x** | $149.85 | Buy 3 → $29.95 each (Save 40%) | $89.85 | $60.00 |
The package's list price stays at $49.95/unit. Each tier is a `count` condition on the Widget package with a `package_percentage` benefit. The API returns the adjusted total so the cart can render before/after pricing automatically.
### Shipping methods [#shipping-methods]
A **shipping method** in a campaign is a virtual link to a shipping method configured on the store, with campaign-specific custom pricing. This lets you charge a different shipping rate (e.g. a flat $4.95 or free shipping) per campaign without changing the underlying store-level shipping method.
### Domains [#domains]
Domains are configured in your store's **Campaign Settings** and apply across all campaigns. They control which domains are authorized to use your campaign API keys.
| Type | SDK Access | Analytics Tracking |
| --------------- | ---------- | ---------------------------------- |
| **Production** | Yes | Yes — events tracked automatically |
| **Development** | Yes | No — events suppressed |
Add `localhost` and any staging domains as **development** domains so you can test the full checkout flow without polluting analytics data.
Requests from domains not listed in either production or development will be rejected. Add every environment your SDK runs in.
***
## Analytics & Tracking [#analytics--tracking]
Every starter template ships with NEXT Campaign Analytics **on by default** in auto mode. The SDK automatically tracks the full funnel — page views, add-to-cart, begin checkout, shipping/payment info, purchase, and upsells — through the always-on internal `nextCampaign` provider. No code required.
To also forward those events to **Google Tag Manager** or **Facebook Pixel**, add your IDs in `_data/campaigns.json`:
```json
{
"my-campaign": {
"name": "My Campaign",
"...": "...",
"gtm_id": "GTM-XXXXXXX",
"fb_pixel_id": "123456789012345"
}
}
```
The matching `providers.gtm.enabled` and `providers.facebook.enabled` flags in `assets/config.js` control whether the SDK **forwards events** to each provider. Templates ship with these set to `false` — flip them to `true` once your IDs are in place. See the per-provider guides below for the full wiring.
Two mechanisms skip tracking entirely — including the always-on internal `nextCampaign` provider — so your campaign dashboards reflect **real customer conversions**, not internal QA or test traffic:
* **Development domains** — any domain marked *development* in your store's Campaign Settings suppresses all events. Add `localhost`, staging URLs, and preview hosts here.
* **`?ignore=true` URL parameter** — append it to any URL to silence tracking for the entire session, useful for one-off testing on a production domain. Clear it with `window.NextAnalyticsClearIgnore()`.
### Event reference [#event-reference]
* [**Analytics events**](https://cart-sdk.nextcommerce.com/latest/reference/analytics-events/) — every `dl_*` event the SDK emits, which ones fire automatically, and what each provider receives
***
## Non-interactive setup (agents, CI) [#non-interactive-setup-agents-ci]
`campaign-init` can run with no prompts — pass every value as a flag:
```bash
npx campaign-init --non-interactive \
--template olympus \
--slug my-campaign \
--name "My Campaign" \
--api-key "$CAMPAIGN_API_KEY" \
--ai-context claude
```
Add `--json` for machine-readable stdout. See the [page-kit CLI reference](https://github.com/NextCommerceCo/campaign-page-kit#non-interactive-agents-ci-scripts) for the full flag list and exit codes.
***
## Hosting [#hosting]
`npm run build` outputs a fully static site to `_site/` — no server, no runtime, just HTML, CSS, JS, and images. That means you can host it on any static host. Configure your provider with:
* **Build command:** `npm run build`
* **Publish directory:** `_site`
Make sure to add the host's domain (e.g. `*.netlify.app`, your custom domain) to your campaign's **authorized domains** in the Campaigns App so the SDK can call the API from that origin.
### Netlify [#netlify]
[Netlify](https://www.netlify.com/) — connect your Git repo, Netlify auto-detects the build command and serves `_site/` on a `*.netlify.app` subdomain (custom domains free). Best for: zero-config Git deploys, branch previews, instant rollbacks.
### Cloudflare Pages [#cloudflare-pages]
[Cloudflare Pages](https://pages.cloudflare.com/) — connect your Git repo, set framework preset to "None", build command to `npm run build`, output directory to `_site`. Best for: global edge network, generous free tier, fast cold starts via Cloudflare's CDN.
### Render [#render]
[Render](https://render.com/) — create a new **Static Site**, point it at your repo, set publish path to `_site`. Best for: a single dashboard alongside any backend services you already run on Render.
### Other static hosts [#other-static-hosts]
The same `_site/` output works on **[Vercel](https://vercel.com/)**, **[GitHub Pages](https://pages.github.com/)**, **[AWS S3 + CloudFront](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html)**, **[Surge](https://surge.sh/)**, or any host that serves static files. There is no server-side rendering and no runtime dependency on Node, so any plain static host will do.
# Page Kit (/docs/campaigns/page-kit)
[Next Campaign Page Kit](https://github.com/NextCommerceCo/campaign-page-kit) is the tooling that turns a directory of static HTML into a fully isolated, multi-campaign workspace — with hot-reload dev, deterministic builds, and a static output you can host anywhere.
## Why Page Kit [#why-page-kit]
Most static site generators are designed around a single site. When you need to manage multiple campaign funnels in one repository, you quickly hit problems: shared layouts bleed across campaigns, asset paths collide, and a change to one campaign silently breaks another.
Page Kit treats each campaign as a **fully isolated unit** within a single repo. Every campaign lives in its own subdirectory with its own layouts, assets, and configuration — but they're all built, versioned, and deployed together. A repo with three campaigns looks like this:
```
my-campaigns/
├── _data/
│ └── campaigns.json # one registry — every campaign is keyed by slug
├── src/
│ ├── espresso-blend/ # each campaign owns its own…
│ │ ├── _layouts/ # …layouts
│ │ ├── _includes/ # …components
│ │ ├── assets/ # …css, js, images, config.js
│ │ ├── checkout.html # …and pages
│ │ ├── upsell.html
│ │ └── receipt.html
│ ├── cold-brew-kit/ # same structure, fully independent
│ │ └── …
│ └── pour-over-set/ # cloning or deleting one never touches the others
│ └── …
└── package.json
```
Nothing is shared between campaigns — editing a layout or stylesheet in one cannot break the others. `npm run build` outputs every campaign to its own URL path (`/espresso-blend/checkout/`, `/cold-brew-kit/checkout/`, …) in a single `_site/` folder.
The CLI tools (`setup`, `dev`, `clone`, `config`, `compress`) and template filters (`campaign_asset`, `campaign_link`, `campaign_include`) enforce this isolation at every step, so you can work on one campaign without fear of affecting another.
## Quick Start [#quick-start]
### 1. Create a project directory [#1-create-a-project-directory]
```bash
mkdir my-campaigns && cd my-campaigns
```
### 2. Initialize and install [#2-initialize-and-install]
```bash
npm init -y
npm install next-campaign-page-kit
```
### 3. Run the setup script [#3-run-the-setup-script]
```bash
npx campaign-init
```
`campaign-init` walks you through everything in one flow:
1. Adds CLI scripts (`dev`, `build`, `clone`, `config`, `compress`, `migrate`, …) to your `package.json`
2. Creates an empty `_data/campaigns.json` registry
3. Fetches the list of available starter templates and shows a picker
4. Asks for your **Campaign name** (display name) and **Campaign slug** (directory + URL path)
5. Downloads only the chosen template's `src//` files into your project
6. Merges the template's registry data into your local `_data/campaigns.json`
7. Optionally prompts for your Campaign API key and writes it to `assets/config.js`
8. Optionally installs an **AI context doc** for your editor or agent
Get your Campaign API key from the Campaigns App in your store. You can skip this step during init and run `npm run config` later.
### 4. Start the dev server [#4-start-the-dev-server]
```bash
npm run dev
```
This will:
1. Show a list of available campaigns
2. Let you pick which campaign to preview
3. Start the dev server
4. Open your browser to the selected campaign
By default the dev server starts on port `3000` and prompts you to pick a campaign.
| Flag | Purpose |
| -------------------------------- | ------------------------------------------------------------------------------ |
| `--campaign `, `-c ` | Skip the picker and start this campaign (must exist in `_data/campaigns.json`) |
| `--port `, `-p ` | Port to listen on, 1–65535 (defaults to `3000`) |
Both flags accept `=`-syntax (`--campaign=my-camp`, `--port=8080`). The first bare positional argument is also accepted as a shortcut: numeric → port, non-numeric → campaign slug. The `PORT` env var sets the port when no flag is given.
```bash
npm run dev # interactive picker, port 3000
npm run dev my-campaign # specific campaign, default port
npm run dev -c my-campaign -p 8080 # specific campaign and port
```
## Non-interactive setup (agents, CI) [#non-interactive-setup-agents-ci]
`campaign-init` can run with no prompts. Pass every value as a flag and add `--non-interactive`:
```bash
npx campaign-init --non-interactive \
--template olympus \
--slug grounding-mat-v2 \
--name "Grounding Mat V2" \
--api-key "$CAMPAIGN_API_KEY" \
--ai-context claude
```
Add `--json` for agent-friendly automation — a single structured object on stdout, all human UI suppressed:
```bash
npx campaign-init --json \
--template olympus --slug grounding-mat-v2 --name "Grounding Mat V2" \
--api-key "$CAMPAIGN_API_KEY" \
--ai-context claude
```
| Flag | Purpose |
| --------------------- | -------------------------------------------------------------------------- |
| `--template ` | Starter template slug (must exist upstream) |
| `--slug ` | Local campaign slug (folder under `src/`, also URL path) |
| `--name <"display">` | Display name (defaults to upstream template name) |
| `--api-key ` | Campaign API key, written to `assets/config.js` |
| `--non-interactive` | Never prompt; missing required input exits with code 5 |
| `--json` | Machine-readable stdout; suppresses all human UI |
| `--dry-run` | Resolve plan; no downloads, no writes |
| `--overwrite` | Replace existing `src//` and registry entry |
| `--ai-context ` | Write AI context doc for `claude`, `codex`, `cursor`, `copilot`, or `none` |
| `--keep-ai-context` | Preserve an existing AI context file |
| `--help`, `-h` | Show full help |
**Exit codes:** `0` ok · `2` template not found · `3` target conflict (use `--overwrite`) · `4` upstream fetch failed · `5` missing required input · `6` invalid input · `7` partial write rolled back · `8` rollback failed.
### AI context [#ai-context]
`--ai-context` writes the upstream context doc verbatim (with a sentinel header) to wherever your tool auto-loads it:
| Tool | Path |
| ------------------ | ---------------------------------------------------------------- |
| **Claude Code** | `CLAUDE.md` at the project root |
| **OpenAI Codex** | `AGENTS.md` at the project root |
| **Cursor** | `.cursor/rules/campaign-page-kit.mdc` (with `alwaysApply: true`) |
| **GitHub Copilot** | `.github/copilot-instructions.md` |
If the tool's file already exists, you're asked whether to update it. The written file always carries a sentinel header noting it was generated by `campaign-init` and will be overwritten on re-run unless you pass `--keep-ai-context`.
## Commands [#commands]
| Command | Description |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `npm start` | Interactive menu: dev server, compress, clone, configure |
| `npm run setup` | Bootstrap a project, install a starter template, set the API key (alias for `campaign-init`) |
| `npm run dev` | Start dev server with interactive campaign picker |
| `npm run build` | Build all campaigns to `_site/` |
| `npm run clone` | Clone an existing local campaign to a new slug |
| `npm run config` | Set the API key for an existing local campaign |
| `npm run compress` | Compress all images in a campaign directory |
| `npm run compress:preview` | Preview compression savings without modifying files |
| `npm run migrate` | Migrate `campaigns.json` from old array format to key-based format |
## Project structure [#project-structure]
```
your-project/
├── _data/
│ └── campaigns.json # Campaign registry (all campaigns)
├── src/
│ └── [campaign-slug]/ # Individual campaign directory
│ ├── _layouts/
│ │ └── base.html # Base layout template
│ ├── _includes/ # Reusable campaign components
│ ├── assets/
│ │ ├── css/
│ │ ├── images/
│ │ ├── js/
│ │ └── config.js # SDK configuration
│ ├── presell.html
│ ├── checkout.html
│ ├── upsell.html
│ ├── receipt.html
│ └── *.html # Any other page
└── package.json
```
**Key files:**
* `_data/campaigns.json` — registers every campaign and its configuration data. Uses a key-based format where each key is the campaign slug. Older projects on the array format can convert with `npm run migrate`.
* `src/[campaign]/_layouts/base.html` — campaign's base layout
* `src/[campaign]/assets/config.js` — Campaign Cart SDK configuration
## Page frontmatter [#page-frontmatter]
Each campaign page uses YAML frontmatter to configure how it renders.
| Field | Type | Required | Description |
| ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page_layout` | string | No | Layout file in `_layouts/`. Defaults to `base.html` |
| `title` | string | Yes | Page title for `` tag |
| `page_type` | string | Yes | `product`, `checkout`, `upsell`, or `receipt` |
| `permalink` | string | No | Custom URL path (e.g., `/starter/`) |
| `next_url` | string | No | Next page in the funnel — the universal forward pointer. Layouts map it to `next-success-url` on checkout pages and `next-upsell-accept-url` on upsell pages. |
| `decline_url` | string | No | Override for upsell decline. Defaults to `next_url`. Maps to `next-upsell-decline-url`. |
| `styles` | array | No | Page-specific CSS (relative paths or external URLs) |
| `scripts` | array | No | Page-specific JS (relative paths or external URLs) |
| `footer` | boolean | No | Show footer on this page |
**Example:**
```yaml
---
page_layout: base.html
title: Checkout
page_type: checkout
next_url: upsell.html
styles:
- https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css
- css/offer.css
scripts:
- https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js
- js/offer.js
footer: true
---
```
### Layout resolution [#layout-resolution]
Layouts resolve to the **current campaign's** `_layouts/` directory:
* `page_layout: base.html` → `/_layouts/base.html`
* `page_layout: custom.html` → `/_layouts/custom.html`
No layout specified? Defaults to `base.html`.
## Campaign context [#campaign-context]
Every page automatically has access to its campaign's data from `_data/campaigns.json` via the `campaign` object — so you can drive copy, links, and config from the registry instead of hardcoding it into templates.
```html
{{ campaign.name }}
Contact: {{ campaign.support_email }}
```
Add any keys you want to your campaign's entry, and they become available immediately:
```json
{
"starter": {
"name": "Starter Campaign",
"entry_url": "presell.html",
"support_email": "support@example.com",
"custom_headline": "Welcome to our Store!"
}
}
```
```html
{{ campaign.custom_headline }}
```
### Reserved fields [#reserved-fields]
These keys have built-in CLI behavior:
| Field | Type | Description |
| ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entry_url` | string | Page that `npm run dev` opens in the browser. Defaults to the campaign root (`//`). Accepts a page name like `presell` or `landing.html` — the path is normalized to `///`. A warning is shown if the page doesn't exist under `src//`. |
### `environment` variable [#environment-variable]
Every template also has access to an `environment` variable indicating the current build mode — useful for conditionally including analytics, debug tools, or other environment-specific content.
| Command | Default |
| --------------- | ------------- |
| `npm run dev` | `development` |
| `npm run build` | `production` |
```html
{% unless environment == "development" %}
{% endunless %}
```
Override the default by setting `CPK_ENV` — useful for build pipelines like Netlify or GitHub Pages where you want a custom value such as `staging`:
```bash
CPK_ENV=staging npm run build
```
## Template filters [#template-filters]
Templates use [Liquid](https://shopify.github.io/liquid/basics/introduction/) syntax. Page Kit adds three custom filters and tags for campaign-relative includes, assets, and links.
Always use these filters instead of hardcoding paths — they're what makes cloning a campaign to a new slug "just work."
### `campaign_asset` [#campaign_asset]
Resolves an asset path to the current campaign.
```html
```
**Use for:** CSS, JS, images, `config.js`, any campaign asset.
### `campaign_link` [#campaign_link]
Generates a clean URL for inter-page navigation within a campaign.
```html
Checkout
Continue
```
The filter strips `.html`, adds a trailing slash, prepends the campaign slug, and passes anchor links (`#section`) and absolute URLs through untouched.
**Use for:** page links, navigation URLs, redirect URLs, SDK meta tags.
### `campaign_include` [#campaign_include]
Includes a file from the **current campaign's** `_includes/` directory.
```html
{% campaign_include 'slider.html' images=slider_images %}
{% campaign_include 'slider.html' images=slider_images show_package_image=true %}
```
**Use for:** reusable components within a campaign (sliders, testimonials, badges).
## Building from scratch (no template) [#building-from-scratch-no-template]
Most users should start with `campaign-init` and pick a starter template. If you'd rather start empty, run `campaign-init` and cancel the template picker (`Ctrl+C`). The bootstrap step still runs — you'll have the CLI scripts and an empty `_data/campaigns.json`.
Then add an entry keyed by slug:
```json
{
"my-campaign": {
"name": "My Campaign",
"description": "My first campaign",
"sdk_version": "0.4.18"
}
}
```
When in doubt, copy the current `sdk_version` from the starter template registry you are matching.
…and create the matching directory tree under `src/`:
```
src/
└── my-campaign/
├── _layouts/
│ └── base.html
├── assets/
│ └── config.js
└── presell.html
```
Then run `npm run config` to set the API key.
## Compressing images [#compressing-images]
Compress every image in a campaign directory in-place. Supports JPEG, PNG, WebP, and GIF. The file is only overwritten if the compressed output is actually smaller.
```bash
npm run compress
```
This will:
1. Show a list of available campaigns
2. Let you pick which one to compress
3. Compress all images anywhere under `src//`
4. Print a before/after table with file sizes and total savings
**Preview mode** — see what would be saved without modifying any files:
```bash
npm run compress:preview
```
Already-optimized images are skipped and reported in a debug line above the summary.
## Building for production [#building-for-production]
```bash
npm run build
```
Output is written to `_site/` — fully static, no server, no runtime. From there, push to any static host. See **Hosting** in [Getting Started](./) for provider-specific setup (Netlify, Cloudflare Pages, Render, Vercel, etc.).
The command exits `1` when any page fails to render, and `0` otherwise. Build warnings (below) never change the exit code.
Make sure the host's domain is listed under your campaign's **authorized domains** in the Campaigns App — otherwise the SDK calls will be rejected from the deployed origin.
### JSON build output (`--json`) [#json-build-output---json]
`campaign-build --json` reports, for every page in the build, which source file was rendered, which URL it resolved to, and which output file it was written to — in a form CI jobs and scripts can consume directly. Available from `next-campaign-page-kit` 0.1.4.
Stdout carries exactly one JSON document and nothing else; warnings, errors, and debug lines go to stderr. Pipe or redirect it without any filtering:
```bash
npm run build -- --json # the -- forwards the flag through npm
npx campaign-build --json | jq '.pages' # query it with jq
npx campaign-build --json > build-output.json # save to a file
```
```json
{
"built": 2,
"errors": 0,
"warnings": 1,
"skipped": 0,
"ms": 312,
"pages": [
{
"inputFile": "src/my-campaign/checkout.html",
"campaignSlug": "my-campaign",
"url": "/my-campaign/checkout/",
"outputFile": "_site/my-campaign/checkout/index.html",
"status": "built",
"warnings": [],
"errors": []
},
{
"inputFile": "src/my-campaign/presell.html",
"campaignSlug": "my-campaign",
"url": "/my-campaign/presell/",
"outputFile": "_site/my-campaign/presell/index.html",
"status": "built",
"warnings": [
{ "code": "MISSING_FRONTMATTER", "message": "missing required frontmatter: page_type" }
],
"errors": []
}
]
}
```
**Top-level fields:**
| Field | Description |
| ---------------------------- | ---------------------------------------------------------- |
| `built`, `errors`, `skipped` | Page counts by outcome — they always sum to `pages.length` |
| `warnings` | Total warning entries across all pages |
| `ms` | Build duration in milliseconds |
**Per-page fields:**
| Field | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `inputFile` | Source file the page was built from, relative to the project root |
| `campaignSlug` | The campaign the page belongs to — its first directory under `src/` |
| `url` | Root-relative URL path the page is served at — the same value templates see as `page.url`. `null` when the build failed before URL resolution |
| `outputFile` | File the rendered page was written to, relative to the project root. `null` under the same condition as `url` |
| `status` | `built` — rendered and written. `error` — failed; see `errors`. `skipped` — slug has no entry in `_data/campaigns.json` |
| `warnings` | Non-fatal findings for this page, each `{ code, message }` |
| `errors` | Why the page failed, each `{ code, message }`. Error codes name the failed step: `READ_ERROR`, `FRONTMATTER_ERROR`, `RESOLVE_ERROR`, `RENDER_ERROR`, `WRITE_ERROR` |
Filter the report to one campaign with jq: `npx campaign-build --json | jq '.pages[] | select(.campaignSlug == "my-campaign")'`
### Build warnings [#build-warnings]
A build can succeed and still be wrong: a misplaced file builds to a different URL than its folder structure suggests, or a typo'd layout name silently renders the page with no layout at all. The build flags these conditions as warnings — printed to stderr in every mode, attached to the affected page in the JSON output, and never fatal.
| Code | Meaning |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NESTED_NO_PERMALINK` | The page file sits in a subdirectory but declares no `permalink`. Routing uses only the campaign slug and the filename, so intermediate directories are dropped: `src/my-campaign/checkout/index.html` builds to `/my-campaign/` — **not** `/my-campaign/checkout/`. Declare a `permalink` to control the URL |
| `DUPLICATE_OUTPUT` | Two source files resolve to the same output file. The page built last silently overwrites the other |
| `LAYOUT_NOT_FOUND` | The layout named in `page_layout` does not exist in `src//_layouts/`, so the page was rendered without any layout |
| `MISSING_FRONTMATTER` | The page is missing `title` or `page_type` in its frontmatter — both are required |
| `INVALID_PAGE_TYPE` | `page_type` is not one of `product`, `checkout`, `upsell`, `receipt` |
| `NO_CAMPAIGN` | The page's slug has no entry in `_data/campaigns.json`, so it was not built (`status` is `skipped`) |
# Templates (/docs/campaigns/templates)
## Introduction [#introduction]
All templates are available for download on GitHub at [NextCommerceCo/campaign-cart-starter-templates](https://github.com/NextCommerceCo/campaign-cart-starter-templates). You can browse the source or clone individual templates directly from the repository.
These are the options you'll see at the `campaign-init` template picker. Each one installs a complete funnel into your project with SDK-ready checkout, cart, upsell, and receipt surfaces. Treat those commerce surfaces as runtime wiring: replace campaign-specific package IDs, offer codes, shipping methods, copy, assets, and routes, but avoid rebuilding the SDK-owned controls from scratch.
# Development Flow (/docs/apps/app-development-flow)
Like any software, Apps require initial development and then ongoing maintenance and improvements. Below is an overview of how to manage develop and release new versions of your app after it's already been installed and live in production.
### Development Stores [#development-stores]
App development stores are stores linked to your app for quick iterations, testing and reviewing functionality.
**App changes are automatically pushed to your development stores**
When developing your app, you'll be using App Kit to `nak build` and `nak push` your latest changes to Next Commerce. Your app will automatically updated on your development store for a quick feedback loop and reviewing your latest changes.
**App changes are not automatically pushed to existing stores with your app installed**
This is to ensure developers have a safe path add new features, test on your development store, and [create a release once](#releases) you're ready. :handshake:
You should always test and verify your latest changes on your development store to make sure that everything is working as expected before creating a release.
### Releases [#releases]
Once you're confident with your app and tested it on your development store, you can create a **Release** on the App detail page in your Partner account. Releases are versioned snapshots of your App that can be installed on public stores and also trigger updates for existing installations. Store's always install the latest version of your app.
#### Versioning [#versioning]
App versions follow [semantic versioning](https://semver.org/) which allows app developers to create and track releases of their app. App version's should always increase to trigger an available update for stores that have already the app installed.
#### App Updates [#app-updates]
If store already has your app installed and you create a new release, existing app installations will be able to update to the latest version.
### Development & Release Flow [#development--release-flow]
Below is a diagram to highlight the workflow for creating your first app, reviewing it on your development store, and creating point releases to distribute your app to production stores.
# App Kit (/docs/apps/app-kit)
App Kit is a command line tool for developers to build and maintain apps that extend storefront themes with [Event Trackers](/docs/apps/event-tracking).
App Kit is only required for Apps that extend to the storefront with App Snippets, [Server to Server Apps](/docs/apps/guides/server-to-server-apps) do not need to use App Kit to complete the Oauth Flow for your App to access the Admin API.
[See Full Instructions on Github](https://github.com/NextCommerceCo/app-kit) or [Install App Kit from PyPi](https://pypi.org/project/next-app-kit/)
## Installation [#installation]
App Kit is a python package available on [PyPi](https://pypi.org/project/next-app-kit/)
If you already have `python` and `pip`, install with the following command:
```bash title="Installation"
pip install next-app-kit
```
#### Mac OSX Requirements [#mac-osx-requirements]
See how to install `python` and `pip` with [HomeBrew](https://docs.brew.sh/Homebrew-and-Python#python-3x). Once you have completed this step you can install using the `pip` instructions above.
#### Windows Requirements [#windows-requirements]
See how to install `python` and `pip` with [Chocolatey](https://python-docs.readthedocs.io/en/latest/starting/install3/win.html). Once you have completed this step you can install using the `pip` instructions above.
## Usage [#usage]
With the package installed, you can now use the commands inside your app directory to build and push your app updates.
| Commands | Description |
| ----------- | ------------------------------------------------------- |
| `nak setup` | Configure current directory with an app in your account |
| `nak build` | build new app zip file |
| `nak push` | push latest app zip file to Next Commerce platform |
#### Setup [#setup]
Configures the current directory with necessary data to push the app files to Next Commerce.
**Data collected by the `setup` command:**
* **App Client ID** - retrieve this from the app in your partner account.
* **Email** - your email used to access your partner account.
* **Password** - your password used to access your partner account.
#### Build [#build]
Creates a new version (zip of the current directory files) to prepare your app to be pushed to Next Commerce.
#### Push [#push]
Pushes the latest version to Next Commerce and to your development stores to review and test your app.
# Assets Reference (/docs/apps/assets)
App Assets are static files included in an App that can be included in Event Trackers or Snippets to extend storefront themes.
**App bundle max size is 2MB**, it's important to minimize and reduce the size of the assets in your App to maintain efficiency. If you are compiling CSS or JS bundles locally, it is recommended to not include the raw source files and only include the compiled minified files.
### Asset Usage Example [#asset-usage-example]
### Supported File Types [#supported-file-types]
| File Extension |
| -------------- |
| `.html` |
| `.json` |
| `.css` |
| `.scss` |
| `.js` |
| `.woff2` |
| `.gif` |
| `.ico` |
| `.png` |
| `.jpg` |
| `.jpeg` |
| `.svg` |
| `.eot` |
| `.tff` |
| `.ttf` |
| `.woff` |
| `.webp` |
| `.mp4` |
| `.webm` |
| `.mp3` |
# Event Tracking (/docs/apps/event-tracking)
Apps can install an [Event Tracker](/docs/storefront/event-tracking) to add storefront ecommerce Event Tracking as part of their integration.
An installed event tracker simplifies the setup flow and makes the app easier for merchants to use, with fewer manual setup steps and simple dashboard configuration.
Event trackers and App snippets are not cross compatible as Event Trackers are loaded in their own sandboxed environment for greater security.
### Add Event Tracker to Manifest [#add-event-tracker-to-manifest]
When building your app, map a javascript file to be installed as an [Event Tracker](/docs/storefront/event-tracking).
```json title="Example Storefront Event Tracker"
"storefront_event_tracker": "tracking.js",
"settings_schema": [
{
"name": "custom_app_id_enabled",
"type": "checkbox",
"label": "Enable Custom App",
"help_text": "",
"default": false
},
{
"name": "custom_app_id",
"type": "text",
"label": "Example Text Setting",
"default": "",
"required": 1,
"help_text": "",
"max_length": 250
}
]
```
When your app is installed, an event tracker will be automatically created with the contents of the JavaScript file mapped to the `storefront_event_tracker` key in your manifest.json.
### Access App Settings Inside Event Tracker [#access-app-settings-inside-event-tracker]
Apps also have [Settings](/docs/apps/settings) that can be used to control how the app works, such as enabling or disabling functionality or adding an ID for the tracking script.
Access the settings keys in your event tracker to read configuration variables in your JavaScript.
```javascript title="Example Settings Usage in Snippet"
// access your app settings through app.settings.
if (app.settings.custom_app_id_enabled) {
console.log(app.settings.custom_app_id);
}
```
# Apps (/docs/apps)
Apps and supporting tools are in Public Beta. If you have questions or run into any issues, don't hesitate to reach out to [support@29next.com](mailto:support@29next.com). More documentation, examples, and tools are on the way.
Apps let you extend built-in functionality of the Next Commerce platform to solve merchant challenges and ship new functionality as an easily installed app.
### Apps Allow You To [#apps-allow-you-to]
#### Extend Core Functionality [#extend-core-functionality]
Use [Webhooks](/docs/webhooks) to subscribe to events and the [Admin API](/docs/admin-api) to add new logic and integrations, see the [Server to Server Guide](/docs/apps/guides/server-to-server-apps).
#### Extend Storefront Themes [#extend-storefront-themes]
Use [Event Tracking](/docs/apps/event-tracking) or [App Snippets](https://developers.nextcommerce.com/docs/apps/snippets) to extend storefront themes, see the [Storefront Extension Guide](/docs/apps/guides/storefront-extension).
### Example Apps [#example-apps]
We have full-featured, open-source example apps that provide complete code examples for many of the concepts needed to build apps.
| Example Apps | Description | Link |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Example S2S App | Server-to-server example covering [OAuth flow](/docs/apps/oauth), [session tokens](/docs/apps/oauth/session-auth), [remote settings](/docs/apps/settings), [webhook setup](/docs/webhooks), and [verification](/docs/webhooks#verifying-webhook-requests). | [View](https://github.com/NextCommerceCo/example-app) |
| Google Analytics 4 | Demonstrates [snippets](/docs/apps/snippets) and [event tracking](/docs/apps/event-tracking) for storefront integrations. | [View](https://github.com/NextCommerceCo/google-analytics-4) |
| Fulfillment Service App | Demonstrates the [OAuth flow](/docs/apps/oauth) and the [fulfillment flow](/docs/apps/guides/fulfillment-service#fulfillment-flow-detail) using the [Fulfillment APIs](/docs/admin-api/reference/fulfillment/fulfillmentOrdersList). | [View](https://github.com/NextCommerceCo/demo-fulfillment-service-app) |
### App Developer Reference Guides [#app-developer-reference-guides]
# Manifest Reference (/docs/apps/manifest)
App manifest.json is used for apps extending the storefront to configure HTML snippets that can be injected into storefront themes through [app\_hooks](/docs/storefront/themes/templates/tags#app_hook). You can also add settings that can generate a settings page in the store dashboard to configure your app in the case your app only extends the storefront and doesn't have a server side integration.
To upload your snippets and manifest.json, install [App Kit](/docs/apps/app-kit) to zip your snippet files and push them to Next Commerce.
## Manifest Reference [#manifest-reference]
The manifest.json file is used to configure your app.
```json title="Example manifest.json"
{
"storefront_event_tracker": "tracking.js",
"locations": {
"storefront": {
"global_header": "snippets/global-header.html"
}
},
"settings_schema": [
{
"name": "enable_app",
"type": "checkbox",
"label": "Enable Custom Optimizer App",
"help_text": "",
"default": false
},
{
"name": "example_setting",
"type": "text",
"label": "Example App Text Setting",
"default": "",
"required": 1,
"help_text": "",
"max_length": 250
}
]
}
```
## Manifest Properties [#manifest-properties]
### storefront\_event\_tracker [#storefront_event_tracker]
Specify a javascript file to install a storefront [Event Tracker](/docs/storefront/event-tracking) to be installed to track user behavior on the storefront and ecommerce related user behavior.
```json title="Example Storefront Event Tracker"
"storefront_event_tracker": "tracking.js",
```
### locations [#locations]
Specifies and maps App Snippets that extend Storefront theme templates. See Theme [app\_hook reference](/docs/storefront/themes/templates/tags#app_hook) for a full list of supported Storefront Theme locations.
```json title="Example locations"
"locations": {
"storefront": {
"global_header": "snippets/global-header.html",
}
}
```
App Hook Locations are limited to only extending a themes header and do not have access to tracking events, consider using an [Event Tracker](/docs/storefront/event-tracking) instead.
### settings\_schema [#settings_schema]
Specifies the settings schema used for storing local settings data within the app that can be referenced in snippets. The settings schema will generate a form available to store admins to add settings for their store. Server side apps also will have access to an App Settings API that can be used to push settings values directly to stores.
```json title="Example settings_schema"
"settings_schema": [
{
"name": "enable_app",
"type": "checkbox",
"label": "Enable Custom App",
"help_text": "",
"default": false
},
{
"name": "example_setting",
"type": "text",
"label": "Example App Text Setting",
"default": "",
"required": 1,
"help_text": "",
"max_length": 250
}
],
```
# Submitting an App for Review (/docs/apps/review)
Congratulations, you've built an app and now it's ready to publish to all Next Commerce customers. Before we publish your app, we need to ensure your app is ready to go.
Private apps can be shared with merchants and installed using the **Install Link** feature available on your app dashboard. Use install links to install and validate your app with merchants before submitting for review.
### App Review Checklist [#app-review-checklist]
* Your app is currently live and on more than 3 merchant stores.
* You've uploaded a logo to your app that is properly sized and displays nicely.
* You've named your app inline with your business and how customers would find your service/app.
* Your app description is 40-60 characters that concisely describes your app.
* Admin API App Requirements:
* Your app requests appropriate Oauth2 permissions for it's use cases instead of a blanket `admin:read` and `admin:write`.
* Your app uses webhooks for listening to events instead of polling the Admin APIs.
* Your app subscribes to the [`app.uninstalled` webhook event](/docs/webhooks#webhook-events) for handling uninstall clean up on your end.
### Submit App for Review [#submit-app-for-review]
Once you've completed all of your app functionality and the checklist items, you're now ready to submit your app for review. Use the link below to submit your app for review.
App Review Form
Our team will review your app to make sure it works properly and is aligned with our expectations.
Once we complete the review we will reach out to you to let you know about the review result. If it is approved, it will appear in the App store for all of our customers. If it is not approved, we will let you know what needs to be fixed/changed for your app to be approved.
# Settings Reference (/docs/apps/settings)
App Settings allow you to define settings that you want users to configure and store with your app. Settings should be defined in your [manifest.json](/docs/apps/manifest) file which will automatically create a form for your app to be configured in the dashboard.
### Example Usage [#example-usage]
At this time, the primary use case of settings is to allow apps to store settings data that can then be used with snippets. This makes it possible to extend storefront theme's natively through the use of that are rendered in themes yet fully contained and controlled by your app. :tada:
```json title="Example Settings Schema"
"settings_schema": [
{
"name": "custom_app_id_enabled",
"type": "checkbox",
"label": "Enable My Custom App",
"help_text": "",
"default": false
},
{
"name": "custom_app_id",
"type": "text",
"label": "Example App Text Setting",
"default": "",
"required": 1,
"help_text": "",
"max_length": 250
}
]
```
You can then use the settings in your app snippets.
```javascript title="Example Settings Usage in Snippet"
// access your app settings through app.settings.
if (app.settings.custom_app_id_enabled) {
console.log(app.settings.custom_app_id);
}
```
For Sever to Server Apps with access to the Admin API, you can update the app settings values stored in the database on the Admin API allowing you to configure the app from your external application.
### Reference [#reference]
| Attribute | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `type` | Yes | Type of form input, see Input Types. |
| `name` | Yes | Name of the setting and key for access in template settings object variable. |
| `label` | Yes | Theme settings form input label. |
| `help_text` | No | Theme settings form input help text that shows below the input. |
| `required` | No | JSON boolean, accepts true or false, false by default. |
| `default` | No | Default value for the setting. |
| `options` | No | List of `key:value` pairs for options. Applicable to radio and select field types for their choices. |
| `max_length` | No | Applicable to `text` field types to limit the length of text input. |
| `max_value` | No | Applicable to number field types to limit the max value. |
| `min_value` | No | Applicable to number field types to set a min value. |
### Input Types [#input-types]
Settings schema input types map to input fields that will be rendered in the theme settings form in the dashboard.
#### text [#text]
```json title="text"
{
"type": "text",
"name": "custom_app_id",
"label": "App Account ID",
"help_text": "Can be found in your app settings.",
"max_length": 250,
"required": true,
"default": ""
}
```
#### textarea [#textarea]
```json title="textarea"
{
"type": "textarea",
"name": "description",
"label": "Description",
"help_text": "Example input textarea",
"default": "Test"
}
```
#### checkbox [#checkbox]
```json title="checkbox"
{
"name": "enable_cookie_msg",
"label": "Enable Cookie Message Pop",
"help_text": "Enable cookie message to site visitors.",
"type": "checkbox",
"default": true
}
```
#### number [#number]
```json title="number"
{
"type": "number",
"name": "homepage_testimonials_count",
"label": "Homepage Number Testimonials to Show",
"help_text": "Control the number of homepage testimonials to show",
"max_value": 10,
"min_value": 0,
"default": 3
}
```
#### email [#email]
```json title="email"
{
"type": "email",
"name": "contact_email_address",
"label": "Public contact email address.",
"help_text": "Email to show in site footer.",
"default": ""
}
```
#### radio [#radio]
```json title="radio"
{
"type": "radio",
"name": "layout",
"label": "Layout Style",
"help_text": "Control the layout style.",
"options": [
{
"name": "Boxed",
"value": "boxed"
},
{
"name": "Full Width",
"value": "full"
}
],
"default": "boxed"
}
```
#### select [#select]
```json title="select"
{
"type": "select",
"name": "header_style",
"label": "Header Style",
"help_text": "Choose header layout style.",
"options": [
{
"name": "Full Width",
"value": "full"
},
{
"name": "Boxed",
"value": "boxed"
},
{
"name": "Overlay",
"value": "overlay"
}
],
"default": "full"
}
```
#### multi-select [#multi-select]
```json title="multi-select"
{
"type": "select",
"multi-select": true,
"name": "accepted_payment_methods",
"label": "Accepted Payment Methods",
"help_text": "Control which payment methods are shown.",
"options": [
{
"name": "Visa",
"value": "visa"
},
{
"name": "Master Card",
"value": "mastercard"
}
],
"default": [
"visa",
"mastercard"
]
}
```
#### url [#url]
```json title="url"
{
"type": "url",
"name": "social_link",
"label": "Social Media Link",
"help_text": "Link to your social media page.",
"default": ""
}
```
#### color [#color]
```json title="color"
{
"type": "color",
"name": "btn_primary_color",
"label": "Primary Button Color",
"help_text": "Primary color for buttons.",
"default": ""
}
```
# Snippets (/docs/apps/snippets)
App Snippets are HTML template files used to extend Storefront Themes.
Snippets follow the same syntax and features of [theme templates](/docs/storefront/themes/templates) which bring a full suite of tools and context available to app developers to leverage when adding custom features to a storefront.
### Locations [#locations]
Theme's on the Next Commerce platform support `app_hooks` which are locations within storefront themes your app can target to include your snippets without needing the customize the theme itself.
To upload your snippets and manifest.json, install [App Kit](/docs/apps/app-kit) to zip your snippet files and push them to Next Commerce.
### Snippet Usage Example [#snippet-usage-example]
# AI Skills (/docs/skills)
[**Next Commerce AI Skills**](https://github.com/NextCommerceCo/skills) are pre-built skills that give AI coding agents deep knowledge of the Next Commerce platform — APIs, CLI workflows, and architecture patterns — so they can work autonomously on your store.
Skills are structured markdown files. Any AI tool that accepts a context file or system prompt can use them — Claude Code, OpenAI Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, and 50+ other LLM-powered agents.
For an optional quickstart that follows the current Campaign Page Kit CLI without choosing a template or project structure for you, use the [campaign agent quickstart](/docs/agent-setup). The quickstart does not require installing a skill.
## Install [#install]
The simplest path is the [`skills` CLI](https://github.com/vercel-labs/skills) — it pulls `SKILL.md` files from a GitHub repo and drops them into the right config directory for whichever assistant you use. The target agent is auto-detected by default.
```bash
# Install every skill from this repo
npx skills add NextCommerceCo/skills
# Install one skill
npx skills add NextCommerceCo/skills -s next-theme-dev
# List skills without installing
npx skills add NextCommerceCo/skills --list
# Target a specific agent (auto-detected by default)
npx skills add NextCommerceCo/skills -a claude-code
```
Once installed, Claude Code auto-detects when a skill is relevant, or you can invoke it directly with `/` (e.g. `/next-theme-dev`). If the skills directory didn't exist before Claude Code started, restart it so it can discover the new directory.
```bash
# Update every installed skill
npx skills update
# Update one skill
npx skills update next-theme-dev
```
For agents the `skills` CLI doesn't support, each `SKILL.md` is plain markdown — load it as a system prompt, context file, or chat upload.
### Ask your AI tool to install [#ask-your-ai-tool-to-install]
You can also let your AI tool drive the install — it knows where files live for the current OS and assistant:
```text
Install the Next Commerce AI skill I need from https://github.com/NextCommerceCo/skills.
Use the installation location for my current AI tool and operating system. If my tool
supports native skills, install each skill as a directory containing its SKILL.md.
If it only supports rules, prompts, or context files, add the relevant SKILL.md there.
Prefer HTTPS clone unless my GitHub SSH access is already configured.
```
Tell it which skill you want, or ask it to inspect [`skills.json`](https://github.com/NextCommerceCo/skills/blob/main/skills.json) and choose the relevant one.
## Available skills [#available-skills]
| Skill | What it does | When to use it |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [**next-theme-figma**](https://github.com/NextCommerceCo/skills/tree/main/next-theme-figma) | Prepare Figma storefront designs for Spark theme implementation — validates source structure, classifies sections and assets, records Spark divergences, and generates a low-inference handoff for `next-theme-dev` | You have a Figma storefront design (PDP, homepage, etc.) you want to turn into a Next Commerce theme |
| [**next-theme-dev**](https://github.com/NextCommerceCo/skills/tree/main/next-theme-dev) | Build and customize storefront themes — DTL templates, ntk CLI, Tailwind CSS, settings, side cart | You're editing theme files, setting up a new storefront, or debugging template issues |
| [**next-campaigns-setup**](https://github.com/NextCommerceCo/skills/tree/main/next-campaigns-setup) | End-to-end CPK campaign setup — scaffolds the project, copies a starter template, seeds `campaigns.json`, wires up the API key, store details, and analytics in one pass | Starting a new CPK campaign for a brand |
| [**next-bulk-fulfill**](https://github.com/NextCommerceCo/skills/tree/main/next-bulk-fulfill) | Update orders to **Fulfilled** with tracking numbers from a CSV | A fulfillment provider shipped orders but tracking didn't sync back — orders stuck in *Processing* |
| [**next-bulk-move**](https://github.com/NextCommerceCo/skills/tree/main/next-bulk-move) | Move fulfillment orders between warehouse locations in bulk — by order-number file or by Product ID / SKU list | Switching fulfillment providers, or moving every FO containing a given SKU/product to a new location |
| [**next-bulk-subscription**](https://github.com/NextCommerceCo/skills/tree/main/next-bulk-subscription) | Apply official actions (pause, cancel) or a PATCH (renewal date, interval, gateway, address) to a list of subscription IDs | Merchant wants to bulk-pause, bulk-shift renewals, bulk-cancel, or migrate subscriptions between gateways |
| [**next-ops-scan**](https://github.com/NextCommerceCo/skills/tree/main/next-ops-scan) | Read-only daily operations risk scan for a store — surfaces Incomplete orders, Rejected orders, and delivery-tracking failures or stale shipments with manual next steps | You want a routine health check to catch risky orders and reduce disputes |
## Prerequisites [#prerequisites]
Each skill lists its own requirements in its `SKILL.md`. Common across all skills:
* Access to a Next Commerce store
* An API key with the scopes specified by the skill (create at **Dashboard → Settings → API Access**)
## Machine-readable index [#machine-readable-index]
For AI agents that need to programmatically discover available skills, [`skills.json`](https://github.com/NextCommerceCo/skills/blob/main/skills.json) is a structured manifest with skill IDs, descriptions, trigger phrases, and prerequisites. Agents can fetch this single file to decide which skill to load.
# Checkout Links (/docs/storefront/checkout-links)
Checkout Links allow you add links from any website, email or web marketing channel directly to your store's checkout flow with items pre-loaded in their cart.
### Example Checkout Links [#example-checkout-links]
#### As a one-time purchase [#as-a-one-time-purchase]
With the link below, 2 products would be added to the cart with an applied voucher.
```bash title="One-Time Purchase"
https://{domain}/checkout/add/?product=12:1&product=13:3&voucher=PROMO¤cy=usd
```
#### As a subscription [#as-a-subscription]
With the following link, 1 product would be added to the cart as a subscription renewing every 3 months.
```bash title="Subscription"
https://{domain}/checkout/add/?product=12:1:3:month¤cy=usd
```
### Supported Parameters [#supported-parameters]
Checkout link parameters can be broadly split into two groups, [Cart Parameters](#cart-parameters) controlling the products and discounts applied to the cart and [Attribution Parameters](#attribution-parameters) to marketing attribution reporting.
#### Cart Parameters [#cart-parameters]
| Parameter | Values | Description |
| ---------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `product` | id:qty:interval\_count:interval | Pass product ID, quantity, interval count (number of) and interval (day, week, month) to add to the cart |
| `voucher` | voucher code | Apply a voucher to add to the cart |
| `currency` | currency code | Set the currency for the cart and product prices |
| `replace` | true/false | replace existing cart, default is true |
#### Attribution Parameters [#attribution-parameters]
In addition to populating the cart, you can pass attribution parameters to attribute the order to your marketing channel.
| Parameter | Description |
| -------------------------------- | ----------------------------------------------------------- |
| `utm_source` | The referrer: (e.g. google, newsletter) |
| `utm_medium` | Marketing medium: (e.g. cpc, banner, email) |
| `utm_campaign` | Product, promo code, or slogan (e.g. spring\_sale) |
| `utm_term` | Identify the paid keywords |
| `utm_content` | Use to differentiate ads |
| `funnel` | Use to attribute funnels |
| `gclid` | Adwords click ID |
| `evclid` | Everflow Click ID |
| `aff` | Main affiliate / network |
| `sub1` | Sub affiliate 1 |
| `sub2` | Sub affiliate 2 |
| `sub3` | Sub affiliate 3 |
| `sub4` | Sub affiliate 4 |
| `sub5` | Sub affiliate 5 |
| `attribution_metadata.KEY=VALUE` | Attribution Metadata key/value pair |
| `clear_attribution=false` | Pass this to only update existing attribution for the cart. |
Attribution on carts and the subsequent orders uses "Last Click" attribution model meaning that each time new attribution is passed it will replace all existing attribution data for the cart. Passing `clear_attribution=false` as a querystring will keep existing attribution and update any new attribution passed.
# Event Tracking (/docs/storefront/event-tracking)
## Overview [#overview]
Event Tracking allows merchants and third-party integrations to subscribe to customer engagement events on your storefront for robust customer behavior tracking.
## Getting Started [#getting-started]
To add new custom Event Trackers, in your store go to Settings > Tracking Events > Add Event Tracker.
```javascript title="Example Product Added to Cart Event"
analytics.subscribe("product_added_to_cart", event => {
console.log(event);
});
```
Go to your storefront and add a product to your cart, you'll now see data from your event tracker logged in the console. :tada:
## Including External Scripts [#including-external-scripts]
Event trackers are pure javascript, meaning third-party event tracking scripts sometimes need some adjustment before they can be added.
```html title="Original Google Analytics HTML Script Tag"
```
Below is the equivalent expressed as a javascript function to create and append the script tag to the document head.
```javascript title="Converetd Javascript Script Tag"
(function() {
var script = document.createElement('script');
script.async = true;
script.src = "https://www.googletagmanager.com/gtag/js?id=G-EXAMPLE";
document.head.appendChild(script);
})();
```
We can now use this to include the Google Analytics javascript in our event tracker.
## Init [#init]
Init is utility method to produce context of the current request/session with details of a `cart`, `store`, or `user` as a JSON object. Use the init method to add context to event tracking integrations.
Use `init.cart` to load context of the current cart data as a JSON object.
```javascript
console.log(init.cart);
```
Init Cart Data
```javascript
{
"id": 1000,
"status": "open",
"lines": [
{
"id": 1000,
"product_id": 111,
"sku": "TIMELESS-WATCH",
"product_title": "Timeless Watch",
"product_image": "https://d36qjeq4w.cloudfront.net/media/..../product.jpg",
"product_url": "https://amazingwidgets.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "Black Band",
"quantity": 2,
"currency": "USD",
"price_excl_tax": "45.76",
"price_incl_tax": "45.76",
"total_discount": "0.00",
"is_upsell": false,
"interval": null,
"interval_count": null,
"metadata": {}
}
],
"abandoned": true,
"total_incl_tax": "45.76",
"total_excl_tax": "45.76",
"total_discount": "0.00",
"currency": "USD",
"user": {
"id": 1000,
"email": "johndoe@gmail.com",
"first_name": "John",
"last_name": "Doe",
"ip": "123.123.123.123",
"phone_number": "+12706814477",
"date_joined": "2023-03-15T06:27:46.253558-04:00",
"language": "en",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...."
},
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"checkout_url": "https://amazingwidgets.com/accounts/complete-order/549167as8232c/",
"created_at": "2025-03-18T05:27:46.765627-04:00",
"metadata": {}
}
```
Use `init.store` to load context of the store public data as a JSON object.
```javascript
console.log(init.store);
```
Init Store Data
```json
{
"name": "Amazing Widgets",
"tagline": "Amazing Widget and Deals",
"timezone": "US/Eastern",
"contact_address": {
"company_name": "Amazing Widgets LLC",
"line1": "2200 Western Pl W",
"line2": "",
"postcode": "42304",
"city": "Hop Top",
"state": "KY",
"country": "US",
"phone_number": "(270) 686-4455"
},
"primary_domain": "amazingwidgets.com",
"tax_id": "",
"available_languages": [
{
"code": "en",
"label": "English"
},
{
"code": "fr",
"label": "Français"
}
],
"available_currencies": [
{
"code": "USD",
"label": "$ USD"
},
{
"code": "CAD",
"label": "CA$ CAD"
}
]
}
```
Use `init.user` to load context of the current authenticated user as a JSON object.
```javascript
console.log(init.user);
```
Init User Data
```json
{
"id": 1000,
"email": "johndoe@gmail.com",
"first_name": "John",
"last_name": "Doe",
"orders_count": 29,
"phone_number": "+12706814477",
"language": "en"
}
```
## Context [#context]
All events have `context` of the parent frame to easily access contextual data about where and how the event occurred.
| Property | Description | Reference |
| ----------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `document` | Data from the top-frame `document` object (e.g., title, referrer). | [Document](https://developer.mozilla.org/en-US/docs/Web/API/Document) |
| `navigator` | Data from the top-frame `navigator` object (e.g., userAgent, language). | [Navigator](https://developer.mozilla.org/en-US/docs/Web/API/Navigator) |
| `window` | Data from the top-frame `window` object (e.g., innerWidth, location). | [Window](https://developer.mozilla.org/en-US/docs/Web/API/Window) |
```json title="Example conext object"
{
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
}
}
```
## Available Tracking Events [#available-tracking-events]
### page\_viewed [#page_viewed]
```javascript
analytics.subscribe("page_viewed", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "page",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {},
"event_type": "page_viewed",
"timestamp": "2024-12-09T06:42:47.750998+00:00",
"event_version": "2024-04-01"
}
```
### product\_category\_viewed [#product_category_viewed]
```javascript
analytics.subscribe("product_category_viewed", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "products",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": [
{
"id": 111,
"url": "https://examplestore.com/catalogue/timeless-watch_111/",
"title": "Timeless Watch",
"slug": "timeless-watch",
"images": [
{
"id": 629,
"attachment": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"caption": "",
"display_order": 0,
"product": 111,
"variants": [112, 113]
}
],
"purchase_info": {
"availability": "available",
"subscription": {
"currency": "USD",
"price": "29.99",
"format": "$29.99"
},
"price": {
"currency": "USD",
"price": "29.99",
"format": "$29.99"
},
"price_retail": {
"currency": "USD",
"price": "39.99",
"format": "$39.99"
}
},
"structure": "parent"
}
],
"event_type": "product_category_viewed",
"timestamp": "2024-12-09T06:44:29.959402+00:00",
"event_version": "2024-04-01"
}
```
### product\_viewed [#product_viewed]
```javascript
analytics.subscribe("product_viewed", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "product",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"id": 111,
"url": "https://examplestore.com/catalogue/timeless-watch_111/",
"title": "Timeless Watch",
"slug": "timeless-watch",
"is_discountable": true,
"is_public": true,
"ranking": 10,
"categories": [
{
"id": 1,
"name": "Example Category",
"slug": "example-category"
}
],
"enable_subscription": true,
"interval": "day",
"interval_counts": [
30,
60,
90
],
"images": [
{
"id": 2,
"original": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"caption": "",
"display_order": 0,
"product": 111
}
],
"requires_shipping": true,
"recommended_products": [
3,
47
],
"upc": "",
"external_tax_code": null,
"variant_attributes": [],
"variants": [
{
"id": 2,
"product_id": 111,
"title": "Timeless Watch",
"sku": "TIMELESS-WATCH-B-BL",
"track_stock": true,
"allow_backorders": false,
"images": [
{
"id": 2,
"original": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"caption": "",
"display_order": 0,
"product": 111
}
],
"variant_attribute_values": [],
"prices": [
{
"currency": "USD",
"price": "79.99",
"retail": "120.00",
"subscription": "69.99",
"subscription_suggested_downsell": "49.99"
}
],
"stockrecords": [
{
"id": 16,
"location_id": 1,
"num_in_stock": 9995,
"num_allocated": 169,
"low_stock_threshold": 500
}
],
"unit_cost": "1.00",
"date_created": "2017-09-28T08:23:08.368000-04:00",
"date_updated": "2024-11-26T03:11:56.747405-05:00",
"metadata": {
"external_id": "12345"
}
}
],
"rating": 5,
"date_created": "2024-01-24T02:02:52.811742-05:00",
"date_updated": "2024-10-03T04:32:52.860659-04:00",
"metadata": {
"excerpt": "
Product conent.
",
"special": "Special Promo",
"external_id": "12345"
}
},
"event_type": "product_viewed",
"timestamp": "2024-12-09T06:46:18.993008+00:00",
"event_version": "2024-04-01"
}
```
### product\_added\_to\_cart [#product_added_to_cart]
```javascript
analytics.subscribe("product_added_to_cart", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "cart_line",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"currency": "USD",
"interval": null,
"interval_count": null,
"is_upsell": false,
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"product_id": 111,
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_title": "Timeless Watch",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"quantity": 1,
"sku": "TIMELESS-WATCH-B-BL",
"total_discount": "0.00",
"variant_id": 2,
"variant_title": ""
},
"event_type": "product_added_to_cart",
"timestamp": "2024-12-09T06:46:36.510579+00:00",
"event_version": "2024-04-01"
}
```
### product\_removed\_from\_cart [#product_removed_from_cart]
```javascript
analytics.subscribe("product_removed_from_cart", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "cart_line",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"currency": "USD",
"interval": null,
"interval_count": null,
"is_upsell": false,
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"product_id": 111,
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_title": "Timeless Watch",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"quantity": 1,
"sku": "TIMELESS-WATCH-B-BL",
"total_discount": "0.00",
"variant_id": 2,
"variant_title": ""
},
"event_type": "product_removed_from_cart",
"timestamp": "2024-12-09T06:47:05.202043+00:00",
"event_version": "2024-04-01"
}
```
### checkout\_started [#checkout_started]
```javascript
analytics.subscribe("checkout_started", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "checkout",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"number": null,
"status": null,
"fulfillment_status": null,
"payment_status": null,
"is_test": null,
"lines": [
{
"product_id": 111,
"sku": "TIMELESS-WATCH-B-BL",
"product_title": "Timeless Watch",
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "",
"quantity": 1,
"currency": "USD",
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"total_discount": "0.00",
"is_upsell": false,
"interval": null,
"interval_count": null
}
],
"shipping_method": null,
"shipping_code": null,
"total_incl_tax": "79.99",
"total_excl_tax": "79.99",
"total_discount": "0.00",
"shipping_incl_tax": null,
"shipping_excl_tax": null,
"total_cost": null,
"total_tax": null,
"shipping_tax": null,
"display_taxes": null,
"currency": "USD",
"user": null,
"shipping_address": null,
"billing_address": null,
"date_placed": null,
"offer_discounts": [],
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"metadata": {},
"transactions": null,
"order_status_url": null
},
"event_type": "checkout_started",
"timestamp": "2024-12-09T06:58:34.676675+00:00",
"event_version": "2024-04-01"
}
```
### checkout\_contact\_info\_submitted [#checkout_contact_info_submitted]
```javascript
analytics.subscribe("checkout_contact_info_submitted", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "checkout",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"number": null,
"status": null,
"fulfillment_status": null,
"payment_status": null,
"is_test": null,
"lines": [
{
"product_id": 111,
"sku": "TIMELESS-WATCH-B-BL",
"product_title": "Timeless Watch",
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "",
"quantity": 1,
"currency": "USD",
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"total_discount": "0.00",
"is_upsell": false,
"interval": null,
"interval_count": null
}
],
"shipping_method": null,
"shipping_code": null,
"total_incl_tax": "79.99",
"total_excl_tax": "79.99",
"total_discount": "0.00",
"shipping_incl_tax": null,
"shipping_excl_tax": null,
"total_cost": null,
"total_tax": null,
"shipping_tax": null,
"display_taxes": null,
"currency": "USD",
"user": {
"id": 123456,
"email": "customer@gmail.com",
"ip": "182.82.112.1",
"first_name": null,
"last_name": null,
"phone_number": null,
"accepts_marketing": true,
"language": "en",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
},
"shipping_address": null,
"billing_address": null,
"date_placed": null,
"offer_discounts": [],
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"metadata": {},
"transactions": null,
"order_status_url": null
},
"event_type": "checkout_contact_info_submitted",
"timestamp": "2024-12-09T06:58:34.676675+00:00",
"event_version": "2024-04-01"
}
```
### checkout\_shipping\_address\_submitted [#checkout_shipping_address_submitted]
```javascript
analytics.subscribe("checkout_shipping_address_submitted", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "checkout",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"number": null,
"status": null,
"fulfillment_status": null,
"payment_status": null,
"is_test": null,
"lines": [
{
"product_id": 111,
"sku": "TIMELESS-WATCH-B-BL",
"product_title": "Timeless Watch",
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "",
"quantity": 1,
"currency": "USD",
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"total_discount": "0.00",
"is_upsell": false,
"interval": null,
"interval_count": null
}
],
"shipping_method": null,
"shipping_code": null,
"total_incl_tax": "79.99",
"total_excl_tax": "79.99",
"total_discount": "0.00",
"shipping_incl_tax": null,
"shipping_excl_tax": null,
"total_cost": null,
"total_tax": null,
"shipping_tax": null,
"display_taxes": null,
"currency": "USD",
"user": {
"id": 123456,
"email": "customer@gmail.com",
"ip": "182.82.112.1",
"first_name": "John",
"last_name": "Doe",
"phone_number": null,
"accepts_marketing": true,
"language": "en",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
},
"shipping_address": {
"id": 7851,
"first_name": "John",
"last_name": "Doe",
"line1": "2200 Western Pl",
"line2": "",
"line3": "",
"line4": "Henderson",
"postcode": "42304",
"phone_number": "+18123158899",
"notes": "",
"state": "KY",
"country": "US"
},
"billing_address": null,
"date_placed": null,
"offer_discounts": [],
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"metadata": {},
"transactions": null,
"order_status_url": null
},
"event_type": "checkout_shipping_address_submitted",
"timestamp": "2024-12-26T06:46:36.936236+00:00",
"event_version": "2024-04-01"
}
```
### checkout\_shipping\_method\_submitted [#checkout_shipping_method_submitted]
```javascript
analytics.subscribe("checkout_shipping_method_submitted", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "checkout",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"number": null,
"status": null,
"fulfillment_status": null,
"payment_status": null,
"is_test": null,
"lines": [
{
"product_id": 111,
"sku": "TIMELESS-WATCH-B-BL",
"product_title": "Timeless Watch",
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "",
"quantity": 1,
"currency": "USD",
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"total_discount": "0.00",
"is_upsell": false,
"interval": null,
"interval_count": null
}
],
"shipping_method": "Express 1-2 Days",
"shipping_code": "express",
"total_incl_tax": "79.99",
"total_excl_tax": "79.99",
"total_discount": "0.00",
"shipping_incl_tax": null,
"shipping_excl_tax": null,
"total_cost": null,
"total_tax": null,
"shipping_tax": null,
"display_taxes": null,
"currency": "USD",
"user": {
"id": 123456,
"email": "customer@gmail.com",
"ip": "182.82.112.1",
"first_name": "John",
"last_name": "Doe",
"phone_number": null,
"accepts_marketing": true,
"language": "en",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
},
"shipping_address": {
"id": 7851,
"first_name": "John",
"last_name": "Doe",
"line1": "2200 Western Pl",
"line2": "",
"line3": "",
"line4": "Henderson",
"postcode": "42304",
"phone_number": "+18123158899",
"notes": "",
"state": "KY",
"country": "US"
},
"billing_address": null,
"date_placed": null,
"offer_discounts": [],
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"metadata": {},
"transactions": null,
"order_status_url": null
},
"event_type": "checkout_shipping_method_submitted",
"timestamp": "2024-12-26T06:46:36.936236+00:00",
"event_version": "2024-04-01"
}
```
### checkout\_completed [#checkout_completed]
```javascript
analytics.subscribe("checkout_completed", event => {
analytics.debugHelper(event.event_type, event);
});
```
Event Data
```json
{
"object": "checkout",
"context": {
"document": {...}, // frame parent document
"navigator": {...}, // frame parent navigator
"window": {...} // frame parent window
},
"data": {
"number": "109659",
"status": "open",
"fulfillment_status": "unfulfilled",
"payment_status": "paid",
"is_test": true,
"lines": [
{
"product_id": 111,
"sku": "TIMELESS-WATCH-B-BL",
"product_title": "Timeless Watch",
"product_image": "https://assets.29nex.store/media/demostore/images/products/2021/03/watch.jpg",
"product_url": "https://examplestore.com/catalogue/timeless-watch_2/",
"variant_id": 2,
"variant_title": "",
"quantity": 1,
"current_quantity": 1,
"fulfillable_quantity": 1,
"currency": "USD",
"price_excl_tax": "79.99",
"price_incl_tax": "79.99",
"total_discount": "0.00",
"unit_cost": "1.00",
"total_cost": "1.00",
"requires_shipping": true,
"is_gift_card": false,
"is_upsell": false
}
],
"shipping_method": "Default",
"shipping_code": "default",
"total_incl_tax": "84.98",
"total_excl_tax": "84.98",
"total_discount": "0.00",
"shipping_incl_tax": "4.99",
"shipping_excl_tax": "4.99",
"total_cost": "1.00",
"total_tax": "0.00",
"shipping_tax": "0.00",
"display_taxes": "",
"currency": "USD",
"user": {
"id": 123456,
"email": "customer@gmail.com",
"ip": "182.82.112.1",
"first_name": "John",
"last_name": "Doe",
"phone_number": null,
"accepts_marketing": true,
"language": "en",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
},
"shipping_address": {
"id": 7851,
"first_name": "John",
"last_name": "Doe",
"line1": "2200 Western Pl",
"line2": "",
"line3": "",
"line4": "Henderson",
"postcode": "42304",
"phone_number": "+18123158899",
"notes": "",
"state": "KY",
"country": "US"
},
"billing_address": {
"id": 7814,
"first_name": "John",
"last_name": "Doe",
"line1": "2200 Western Pl",
"line2": "",
"line3": "",
"line4": "Henderson",
"postcode": "423014",
"phone_number": "+18123158899",
"state": "KY",
"country": "US"
},
"date_placed": "2024-12-09T02:00:20.931086-05:00",
"offer_discounts": [],
"voucher_discounts": [],
"attribution": {
"agent": null,
"funnel": null,
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_term": null,
"utm_content": null,
"gclid": null,
"metadata": {},
"affiliate": null,
"subaffiliate1": null,
"subaffiliate2": null,
"subaffiliate3": null,
"subaffiliate4": null,
"subaffiliate5": null
},
"metadata": {},
"transactions": [
{
"id": 8127,
"external_id": "109659",
"type": "debit",
"amount": "84.98",
"currency": "USD",
"parent_id": null,
"status": "succeeded",
"date_created": "2024-12-09T02:00:21.254596-05:00",
"payment_method": "bankcard",
"payment_details": {
"gateway": {
"id": 2,
"type": "test",
"name": "Test"
},
"bankcard_first_six": "411111",
"bankcard_last_four": "1111",
"is_3ds": false,
"optimized_3ds": false,
"downgrade_3ds_retry": false,
"sca_flow": null,
"card_token": "01JEN3G3VP12C9KC75B"
},
"response_code": 1000,
"is_disputed": false,
"is_external": false,
"is_test": true,
"is_initial_retry": false,
"report_values": {
"currency": "USD",
"amount": "84.98"
}
}
],
"order_status_url": "https://examplestore.com/accounts/order-status/109659/109659:VoL9PVdtkdDFq-Iog81_fQvBeiHjJdqgfhgDi6mvGg4/"
},
"event_type": "checkout_completed",
"timestamp": "2024-12-09T07:00:22.284164+00:00",
"event_version": "2024-04-01"
}
```
# Storefront (/docs/storefront)
The Next Commerce storefront is a flexible, customizable front-end layer for your ecommerce business. Whether you're building a completely custom storefront or enhancing an existing theme, this section will guide you through the tools and features available to developers.
### Themes [#themes]
Themes allow you to fully control the look and feel of your storefront using modern front-end technologies. Each theme includes layouts, templates, stylesheets, and scripts, giving you full creative freedom to build a branded customer experience.
* Customize homepage, products, catalog, and pages
* Easily manage theme assets (CSS, JS, images)
* Customize themes via the dashboard or CLI (Theme Kit)
Learn how to [build and manage your theme →](/docs/storefront/themes)
### Event Tracking [#event-tracking]
Capture user behavior, conversion events, and key storefront interactions with built-in event tracking tools.
* Track add-to-cart, purchases, and other standard ecommerce events
* Hook into page views, checkout events, and custom triggers
* Integrate storefront events with external platforms
Learn how to [implement tracking for your storefront →](/docs/storefront/event-tracking)
### Storefront GraphQL API [#storefront-graphql-api]
Power deeper customizations, dynamic content loading, and headless experiences using our Storefront GraphQL API.
* Fetch products, cart data, and more in real-time.
* Create custom functionality such as cart upsells.
Learn how to [query your storefront data →](/docs/storefront/graphql)
# Testing (/docs/testing)
You can test every order flow on Next Commerce — checkout, upsells, subscriptions, webhooks — **on a live store, without moving real money and without changing any payment settings**. Test cards create tagged Test Orders that never touch a payment gateway, and Test Orders can be deleted when you're done.
There is no separate sandbox environment to provision. Test cards work on live stores and live integrations, so you QA against the exact configuration that will serve real customers.
## Test Cards [#test-cards]
Use these card numbers in any checkout — storefront, campaign funnel, or API-driven — to create Test Orders with no attached transactions:
| Test Card Number | Expiration | CVV | Use Case |
| ---------------- | --------------- | --- | ---------------------------------------------- |
| 6011111111111117 | Any Future Date | Any | Test payment success flow without transaction. |
| 6011000990139424 | Any Future Date | Any | Test 3DS payment flow without transaction. |
Orders created this way are tagged as **Test Orders** in the dashboard, skip the gateway entirely, and are safe to create on stores with live traffic.
On the Admin API, you can skip card tokenization during early integration work by using the `test_card` and `test_3ds_card` tokens directly — see the [Testing Guide](/docs/admin-api/guides/testing-guide) for the token flow.
## Testing Checkout and Campaign Funnels [#testing-checkout-and-campaign-funnels]
To QA a campaign funnel (landing → checkout → upsell → receipt) end to end, add your development domain (e.g. `localhost`) as a *development* domain in the Campaigns App so requests are authorized, then pay with a test card at checkout. The [Campaigns getting-started guide](/docs/campaigns) walks through this, including placing a complete test order.
Upsell and post-purchase flows work the same way: the test card carries through the whole funnel, so every accept/decline path can be exercised without a real charge.
## Testing Full Transaction Flows [#testing-full-transaction-flows]
Test cards create orders with **no** transactions. If your integration needs realistic transaction objects — captures, declines, refunds — set up the **Test Gateway** (Settings > Payments > Add Gateway), which behaves like a real gateway and produces Test transactions, including a dedicated declined-payment card.
Adding the Test Gateway to your default gateway group can affect live order flows. On stores with live traffic, prefer test cards, or target the Test Gateway by ID through the API. See [Test Gateway setup](/docs/admin-api/guides/testing-guide).
## Testing Subscriptions and Renewals [#testing-subscriptions-and-renewals]
You don't have to wait calendar time to test a renewal:
* Create a test subscription through the [Test Gateway](/docs/admin-api/guides/testing-guide) (Test Gateway subscriptions can create renewal orders; test-card subscriptions cannot).
* Trigger a renewal on demand with the [subscriptionsRenewCreate](/docs/admin-api/reference/subscriptions/subscriptionsRenewCreate) endpoint, or set `next_renewal_date` to a past date and the renewal processes within about 30 minutes. Both are covered in [Subscription Management](/docs/admin-api/guides/subscription-management).
## Testing Webhooks [#testing-webhooks]
Webhook payloads mirror Admin API serializers, so the data your receiver gets matches what a GET on the same resource returns. Set up a test webhook and inspect delivery logs from the dashboard to verify your receiver, and drive events with your test order flows — see the [Webhooks overview](/docs/webhooks) for payload structure, signature verification, and retry behavior.
## Cleaning Up [#cleaning-up]
Test Orders are identifiable and deletable, so QA runs don't pollute reporting. Filter for Test Orders in the dashboard to review or remove them once you're done.
# Webhooks (/docs/webhooks)
Use webhooks to be notified about events that happen in your store.
Stores can send webhooks that notify your application anytime an event happens. This is especially useful for building custom reporting solutions that need to receive data on order or customer activity.
### Why Webhooks [#why-webhooks]
Webhooks are an efficient way to sync data from a store in near real time, keeping your app up to date without the overhead of traditional polling. See the example below for subscribing to `order.created` events.
### Use Cases [#use-cases]
Common use cases include, but are not limited to:
* Integrating with external marketing platforms
* Collecting data for external reporting applications
* Integrating with external fulfillment services
* Integrating with dispute management services
### Setting Up Webhooks [#setting-up-webhooks]
You can register new webhooks through **Settings > Webhooks** or the Admin API to send event data to your application endpoint. For each webhook, you can subscribe to all events or select specific events to send to your endpoint. See a list of all events and example event data below.
Webhook target endpoints must accept JSON data and respond with a `200` response code. If we do not receive a `200` response, we will retry up to 10 times over a several-day period on an exponential backoff schedule.
Webhook handlers should also complete within about 20 seconds. Longer-running endpoints can be treated as failed deliveries and retried.
**Failing webhooks will trigger email notifications to all store admins and will eventually be deactivated.**
Returning a `410` response code indicates the target resource is no longer available and will automatically disable the webhook.
### Delivery Guarantees [#delivery-guarantees]
Webhooks are delivered at least once. Every event is queued and retried until your endpoint returns a `200`. The `event_id` stays the same across retries of the same event, so you can use it to skip events you've already processed.
There is no delivery time guarantee. Events are delivered from a queue, so timing depends on queue volume and any retries. We autoscale during busy periods to keep delays to a minimum, but receivers should not rely on events arriving within a specific timeframe.
Because events can arrive out of order, use the timestamps in the payload data to determine sequence. If your app needs the current state of a resource, retrieve it from the [Admin API](/docs/admin-api).
We also recommend processing events asynchronously so that spikes in delivery volume don't overwhelm your endpoint.
### Webhook Events [#webhook-events]
| Event | Description | Reference |
| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `app.uninstalled` | Triggers when an app is uninstalled. *Only available for apps.* | [View Example](/docs/webhooks/reference/apps/app.uninstalled) |
| `cart.abandoned` | Triggers when a cart is marked as abandoned. | [View Example](/docs/webhooks/reference/carts/cart.abandoned) |
| `customer.created` | Triggers when a new customer is created. | [View Example](/docs/webhooks/reference/customers/customer.created) |
| `customer.redacted` | Triggers when a customer is redacted. | [View Example](/docs/webhooks/reference/customers/customer.redacted) |
| `customer.updated` | Triggers when an existing customer is updated. | [View Example](/docs/webhooks/reference/customers/customer.updated) |
| `dispute.created` | Triggers when a new dispute is created. | [View Example](/docs/webhooks/reference/payments/dispute.created) |
| `dispute.updated` | Triggers when a dispute is updated. | [View Example](/docs/webhooks/reference/payments/dispute.updated) |
| `export.created` | Triggers when an export is available for download. | [View Example](/docs/webhooks/reference/exports/export.created) |
| `fulfillment.created` | Triggers when a new fulfillment is created. | [View Example](/docs/webhooks/reference/fulfillment/fulfillment.created) |
| `fulfillment.updated` | Triggers when a fulfillment is updated. | [View Example](/docs/webhooks/reference/fulfillment/fulfillment.updated) |
| `gateway.created` | Triggers when a new gateway is created. | [View Example](/docs/webhooks/reference/payments/gateway.created) |
| `gateway.updated` | Triggers when a gateway is updated. | [View Example](/docs/webhooks/reference/payments/gateway.updated) |
| `order.created` | Triggers when an order is created. | [View Example](/docs/webhooks/reference/orders/order.created) |
| `order.updated` | Triggers when an existing order is updated. | [View Example](/docs/webhooks/reference/orders/order.updated) |
| `product.created` | Triggers when a product is created. | [View Example](/docs/webhooks/reference/products/product.created) |
| `product.deleted` | Triggers when an existing product is deleted. | [View Example](/docs/webhooks/reference/products/product.deleted) |
| `product.updated` | Triggers when an existing product is updated. | [View Example](/docs/webhooks/reference/products/product.updated) |
| `transaction.created` | Triggers when a payment transaction is created. | [View Example](/docs/webhooks/reference/payments/transaction.created) |
| `transaction.updated` | Triggers when a payment transaction is updated. | [View Example](/docs/webhooks/reference/payments/transaction.updated) |
| `subscription.created` | Triggers when a new subscription is created. | [View Example](/docs/webhooks/reference/subscriptions/subscription.created) |
| `subscription.updated` | Triggers when an existing subscription is updated. | [View Example](/docs/webhooks/reference/subscriptions/subscription.updated) |
| `store.updated` | Triggers when store settings are updated. | [View Example](/docs/webhooks/reference/store/store.updated) |
| `ticket.created` | Triggers a new support ticket is created. | [View Example](/docs/webhooks/reference/support/ticket.created) |
| `ticket.updated` | Triggers when an existing support ticket is updated. | [View Example](/docs/webhooks/reference/support/ticket.updated) |
The `subscription` object on a `transaction.created` webhook links the charge to its subscription and identifies the billing cycle. See [Identifying Subscription Charges](/docs/admin-api/guides/subscription-management#identifying-subscription-charges).
There is no renewal-specific event. A renewal charge arrives as `transaction.created` with `billing_cycle` of 1 or higher on that `subscription` object; the initial charge has `billing_cycle` 0.
Chargebacks and pre-chargeback alerts both arrive as `dispute.created` and `dispute.updated`; the dispute's `type` field says which it is. See the [Disputes guide](https://docs.nextcommerce.com/docs/features/payments/disputes-guide).
### Webhook Data Structure [#webhook-data-structure]
Webhook payloads follow the same structure as Admin API data serializers, which makes them predictable. In general, the data in a webhook payload matches the data you would get by retrieving the same resource through the API. You can set up test webhooks and view the webhook logs in the dashboard to help build and verify your receiver.
```json title="Webhook Event Payload Structure"
{
"object": "",
"data": "",
"event_id": "",
"event_type": "",
"webhook": "",
"api_version": "2023-02-10"
}
```
Below is a full example of a webhook payload for a `customer.created` event to demonstrate.
```json title="Example Webhook Event Data"
{
"api_version": "2023-02-10",
"data": {
"accepts_marketing": true,
"addresses": [],
"date_joined": "2021-12-17T14:52:53.715787+07:00",
"email": "testing@testing.com",
"first_name": "Tester",
"id": 32234664,
"ip": null,
"is_blocked": false,
"language": "en",
"last_name": "Test",
"orders_count": 0,
"phone_number": null,
"subscriptions_count": 0,
"tags": [],
"total_spent": null,
"user_type": "lead"
},
"event_id": "f7eb1338-0934-4cda-8128-d6a77761a368",
"event_type": "customer.created",
"object": "customer",
"webhook": {
"events": [
"customer.created"
],
"id": 39,
"store": "storename",
"target": "https://webhook.site/6a880c2a-48db-4e28-a575-294dfee934234"
}
}
```
### Webhook API Versions [#webhook-api-versions]
Webhook object data structure follows the [Admin API](/docs/admin-api) and Admin API versioning to ensure predictable data structure for existing webhook receiver endpoints with a path for upgrades.
**Handling Webhook API Versions**
Your app can add handling logic using the `api_version` key when receiving and processing data to handle multiple webhook data structures while upgrading to a newer webhook api version.
### Verifying Webhook Requests [#verifying-webhook-requests]
Webhook endpoints are generally open to the internet and therefore it's a best practice to verify the payload data.
Webhook requests include a header `X-29Next-Signature`, the value is a signature of the webhook payload signed using the webhook signing secret. Your application can use the signature to verify the payload authenticity, see an example below.
```python title="Verifying Webhook Payload"
import json
webhook_secret =
def webhook_payload_validator(request):
request_sig = request.headers.get('X-29Next-Signature', None)
webhook_data = json.loads(request.body)
expected_sig = hmac.new(
webhook_secret.encode(),
json.dumps(webhook_data).encode(), hashlib.sha256
).hexdigest()
return True if expected_sig == request_sig else False
```
As shown above, we can verify the data by generating the same signature with the webhook secret.
When creating webhooks on the API, you can provide your own signing secret to simplify the signature verification process for webhook payloads.
# Update a Campaign Package Image (/docs/admin-api/guides/campaign-package-images)
Each campaign package has an image that your funnel pages show to customers. A package starts with its product's catalogue image, and this guide covers replacing it with a campaign specific image, or removing it, through the Admin API.
Requests need the `campaigns:write` scope. Listing packages to find their IDs needs `campaigns:read`.
### Update Flow [#update-flow]
Replacing a package image is a 3 step process:
1. Find the package `id` on the campaign using the [campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) endpoint.
2. Send the new image to the [campaignsPackagesImageUpdate](/docs/admin-api/reference/campaigns/campaignsPackagesImageUpdate) endpoint, from either a URL or base64 data.
3. Read the new `image` URL back from the response.
### Find the Package [#find-the-package]
[campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) returns every package on the campaign and filters on `name`, `product_name`, and `product_sku`.
```json title="__http:GET:https://{store}.29next.store/api/admin/campaigns/{id}/packages/?product_sku=WIDGET-BLU"
{}
```
Each package in the response carries its `id` and current `image` URL.
### Update From a URL [#update-from-a-url]
Pass `src` and the platform fetches the image from that URL.
```json title="__http:PUT:https://{store}.29next.store/api/admin/campaigns/{id}/packages/{packageId}/image/"
{
"src": "https://cdn.example.com/widget-single-hero.png", // URL to fetch the image from
"file_name": "widget-single-hero" // optional, the extension is taken from the image
}
```
### Update From Base64 Data [#update-from-base64-data]
Pass `attachment` with the base64-encoded file when the image is not hosted anywhere your store can reach.
```json title="__http:PUT:https://{store}.29next.store/api/admin/campaigns/{id}/packages/{packageId}/image/"
{
"attachment": "iVBORw0KGgoAAAANSUhEUgAA...", // base64-encoded image
"file_name": "widget-single-hero" // optional, the extension is taken from the image
}
```
The request accepts one source per call. Accepted formats are JPG, JPEG, PNG, ICO, GIF, and WebP. Two limits apply independently: 10 MB file size and 25 megapixels (around 5000 x 5000). If you omit `file_name`, a name is generated automatically.
### Read the Result [#read-the-result]
The response is the full package object, so the new `image` URL comes back on the same call with no follow-up retrieve.
```json title="Response"
{
"id": 2231,
"name": "Widget Single",
"image": "https://d36qjeq4w.cloudfront.net/media/.../widget-single-hero.png",
"product_id": 184,
"product_variant_id": 512,
...
}
```
### Remove an Image [#remove-an-image]
[campaignsPackagesImageDestroy](/docs/admin-api/reference/campaigns/campaignsPackagesImageDestroy) removes the image from the package and returns `204` with no body.
```json title="__http:DELETE:https://{store}.29next.store/api/admin/campaigns/{id}/packages/{packageId}/image/"
{}
```
The [Campaign Cart API](/docs/campaigns/api) returns the same URL as `image` on each package in the campaign response, and as `package_image` on cart and order lines. Funnel pages that render package images pick up the new image without a deploy.
See [Campaigns Admin API](/docs/campaigns/admin-api) for creating campaigns and packages end to end.
# API Exports (/docs/admin-api/guides/exports)
The Exports API allows you to generate and download bulk data exports from your store as CSV files. This is useful for reporting, analytics, reconciliation, and data migration workflows.
Exports are processed asynchronously. After creating an export, you'll need to poll for completion before downloading the file.
### Export Flow [#export-flow]
Creating and downloading an export is a 3-step process:
1. Subscribe to the `export.created` [webhook event](/docs/webhooks) to be notified when exports are ready.
2. Create a new export using the [exportsCreate](/docs/admin-api/reference/exports/exportsCreate) endpoint with your desired type and date range.
3. When you receive the `export.created` webhook, download the file using the [exportsDownloadRetrieve](/docs/admin-api/reference/exports/exportsDownloadRetrieve) endpoint.
### Available Export Types [#available-export-types]
Use the [exportsTypesRetrieve](/docs/admin-api/reference/exports/exportsTypesRetrieve) endpoint to list all available export types, or reference the table below.
| Type | Description |
| ------------------------- | ----------------------- |
| `order_list` | Orders |
| `order_line_items` | Order Line Items |
| `customer_list` | Customers |
| `transaction_list` | Transactions |
| `dispute_list` | Disputes |
| `subscription_list` | Subscriptions |
| `subscription_line_items` | Subscription Line Items |
| `open_cart_list` | Open Carts |
| `open_cart_line_items` | Open Cart Line Items |
| `return_list` | Returns |
| `return_line_items` | Return Line Items |
| `fulfillment_list` | Fulfillments |
| `fulfillment_line_items` | Fulfillment Line Items |
### Create an Export [#create-an-export]
To create a new export, send a POST request to the [exportsCreate](/docs/admin-api/reference/exports/exportsCreate) endpoint with the export `type` and date range.
```json title="__http:POST:https://{store}.29next.store/api/admin/exports/"
{
"type": "order_list", // export type
"date_from": "2025-01-01T00:00:00Z", // start of date range
"date_to": "2025-03-31T23:59:59Z" // end of date range
}
```
The response returns the export object with a `pending` status.
```json title="Create Export Response"
{
"id": 42,
"created_at": "2025-04-01T12:00:00Z",
"type": "order_list",
"status": "pending",
"date_from": "2025-01-01T00:00:00Z",
"date_to": "2025-03-31T23:59:59Z",
"url": "https://{store}.29next.store/api/admin/exports/42/"
}
```
### Poll Export Status [#poll-export-status]
After creating an export, poll the [exportsRetrieve](/docs/admin-api/reference/exports/exportsRetrieve) endpoint until the `status` changes from `pending` to `available`.
```json title="__http:GET:https://{store}.29next.store/api/admin/exports/{id}/"
{
"id": 42,
"created_at": "2025-04-01T12:00:00Z",
"type": "order_list",
"status": "available", // export is ready to download
"date_from": "2025-01-01T00:00:00Z",
"date_to": "2025-03-31T23:59:59Z",
"url": "https://{store}.29next.store/api/admin/exports/42/"
}
```
Avoid polling too frequently. Checking every couple of minutes is sufficient for pending exports. For a more efficient approach, subscribe to the `export.created` [webhook event](/docs/webhooks) to be notified immediately when an export is available for download.
### Download Export File [#download-export-file]
Once the export status is `available`, use the [exportsDownloadRetrieve](/docs/admin-api/reference/exports/exportsDownloadRetrieve) endpoint to get a download URL for the CSV file.
```json title="__http:GET:https://{store}.29next.store/api/admin/exports/{id}/download/"
{
"url": "https://example.com/media/{store}/export_csv/{file_name}.csv?signature" // signed download
}
```
Download URLs are temporary signed URLs. Fetch the file promptly after retrieving the URL. If the URL expires, request a new one from the download endpoint.
### Export Webhooks [#export-webhooks]
Subscribe to the `export.created` [webhook event](/docs/webhooks) to be notified immediately when an export is available for download. The webhook payload includes the export object data, allowing you to proceed directly to downloading the file without additional API calls.
Using the `export.created` webhook is the recommended approach for automated export workflows. It eliminates the need for polling and ensures you download the file as soon as it's ready.
### List Exports [#list-exports]
Use the [exportsList](/docs/admin-api/reference/exports/exportsList) endpoint to retrieve previous exports with optional filtering by type and creation date.
```json title="__http:GET:https://{store}.29next.store/api/admin/exports/"
{
"next": null,
"previous": null,
"results": [
{
"id": 42,
"created_at": "2025-04-01T12:00:00Z",
"type": "order_list",
"status": "available",
"date_from": "2025-01-01T00:00:00Z",
"date_to": "2025-03-31T23:59:59Z",
"url": "https://{store}.29next.store/api/admin/exports/42/"
}
]
}
```
The list endpoint uses cursor-based pagination. Use the `next` and `previous` URLs in the response to navigate through results.
# External Checkout Flow (/docs/admin-api/guides/external-checkout)
In this guide we'll cover the best practices when building an external checkout flow using the Next Commerce Admin API. Using the API gives you the most flexibility of data and functionality across the platform and external integrations.
### Create Cart [#create-cart]
Carts are the starting point for all orders, carts are essentially draft orders waiting to be converted into orders. Creating the cart is a vital step in capturing leads and setting up abandoned cart flows.
```json title="__http:POST:https://{store}.29next.store/api/admin/carts/"
{
"lines": [
{
"product_id": 1,
"currency": "USD",
"quantity": 1,
"price": 33.44 // optional
}
],
"user": {
"email": "johndoe@gmail.com",
"first_name": "John",
"last_name": "Doe",
"language": "en",
"ip": "1.1.1.1",
"accepts_marketing": true,
"user_agent": "Mozilla/5.0..."
},
"attribution": {
"funnel": "Funnel Offer V2",
"metadata": {
"custom_meta_field": "Custom meta data"
}
}
}
```
* The carts create API accepts a user object that will `get or create` the user by their email address making it is safe to use for both new and existing users.
* Attribution added to a cart is carried over to the Orders and Subscriptions created on the users next order, you do not need to pass this data on the order request.
* If you need to capture an address with the cart, use the [usersAddressesCreate](/docs/admin-api/reference/customers/usersAddressesCreate) endpoint which will create a default address for the user.
### Create Order [#create-order]
Creating an order is the core resource in an external checkout flow, see the example below to familiarize yourself with the [ordersCreate](/docs/admin-api/reference/orders/ordersCreate) API endpoint.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/"
{
"lines": [
{
"product_id": 1,
"currency": "USD",
"quantity": 1,
"price": "5.99" // optional custom price
}
],
"user":{
"email": "johndoe@gmail.com"
},
"shipping_code": "default",
"shipping_price": "5.48",
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"line1": "9975 Berkshire Dr.",
"line4": "Monsey",
"postcode": "10952",
"phone_number": "2025550140",
"state": "NY",
"country": "US"
},
"billing_same_as_shipping_address": true,
"payment_method": "card_token",
"payment_details": {
"card_token": "test_card"
}
}
```
* Complete User detail is not necessary if the user already exists, you can reference an existing user by `id` or by `email`.
* Attribution detail are not necessary if you have already added it to the users active cart.
* `shipping_code` and `shipping_price` are optional parameters you can set to specify the shipping method and shipping price.
* If you already created an address for the user, pass `use_default_shipping_address` and `use_default_billing_address` as true to use their default address for the order.
* For additional payment methods, see the [Payment Methods](/docs/admin-api/guides/payment-methods) guides — [Bankcard](/docs/admin-api/guides/payment-methods/bankcard), [3DS2](/docs/admin-api/guides/payment-methods/bankcard#3d-secure-3ds2), [Apple Pay](/docs/admin-api/guides/payment-methods/apple-pay), [PayPal](/docs/admin-api/guides/payment-methods/paypal), [Klarna](/docs/admin-api/guides/payment-methods/klarna), [Affirm](/docs/admin-api/guides/payment-methods/affirm), [Afterpay](/docs/admin-api/guides/payment-methods/afterpay), [Twint](/docs/admin-api/guides/payment-methods/twint), [Swish](/docs/admin-api/guides/payment-methods/swish), [Bancontact](/docs/admin-api/guides/payment-methods/bancontact), and [SEPA](/docs/admin-api/guides/payment-methods/sepa-debit).
### Add Upsells [#add-upsells]
Add additional products (line items) to the original order through the [ordersAddLineItemsCreate](/docs/admin-api/reference/orders/ordersAddLineItemsCreate) API. Adding items to the order will automatically re-use the order's initial payment method to collect payment for the additional products.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/add-line-items/"
{
"lines": [
{
"product_id": 2,
"currency": "USD",
"quantity": 1,
"price": "10.33" // optional custom price
}
]
}
```
The `ordersAddLineItemsCreate` API requires that the order initial **payment method** supports merchant initiated charges. Current supported payment methods are `bankcard` and `paypal` (with Reference Transactions enabled).
### Cart / Order / Upsell lines Detail [#cart--order--upsell-lines-detail]
Cart, Order, and Upsell line items represent the products the customer is purchasing. When an order is created, the product fulfillment location will be automatically chosen based on product stock records and inventory availability.
```json title="Specify Line Items and Currency"
{
"lines": [
{
"product_id": 1, // ensure variant ID and not parent product ID
"currency": "USD",
"quantity": 1,
"price": "33.44" // optional custom price
}
]
}
```
Ensure you use the variant product ID for order line items. **Single variant products still contain a variant product future product management to add additional variants.**
#### Subscription Line Items [#subscription-line-items]
Lines also accept an optional `subscription` object to specify a subscription that will be automatically created after the initial order is successfully created.
```json title="Subscription Line Items"
"lines": [
{
"product_id": 1,
"currency": "USD",
"quantity": 1,
"price": "5.99", // optional custom initial price
"subscription": {
"interval": "day", // day, month, year
"interval_count": 30, // interval counter, ie 30 days
"price": "24.99" // optional custom recurring price
}
}
]
```
Subscription line items have two `price` fields available. The order line item level `price` and then the subscription object `price`. Passing price points in these fields enables you to easily achieve an "initial order discount".
Orders with Subscription line items **must have an initial payment** (order total > 0.00) to validate and retain the bankcard for future usage.
#### Custom Line Item Properties [#custom-line-item-properties]
Lines accept an optional `properties` object of key/value pairs to capture customization details for personalized or made-to-order products, such as an engraving, monogram, or gift message. The values are stored on the line item and persist to the created order (and to the subscription line item when the line creates a subscription).
```json title="Custom Line Item Properties"
"lines": [
{
"product_id": 1,
"quantity": 1,
"properties": {
"engraving": "Best Dad Ever",
"font": "Serif",
"gift_message": "Happy Birthday!"
}
}
]
```
Line item `properties` are used to determine the uniqueness of line item products and flow all the way through to fulfillment order line items for customized product fulfillment.
### Cart / Order User Detail [#cart--order-user-detail]
The `user` object on carts/orders represents the **customer** which includes their contact details. Below are the recommended fields to pass for the user for ease of use and support of external integrations that may rely on the data, i.e. `user_agent`.
Users are first checked for an existing user by `email` before creating a new user. Successive `cartsCreate` and `ordersCreate` API requests with the same user details will reference the same user.
```json title="User Detail"
"user":{
"first_name": "John",
"last_name": "Doe",
"email": "johndoe@gmail.com",
"phone_number": "+18125879988", // optional, E.164 format required
"language": "en", // used for localized email notifications
"accepts_marketing": true, // used by external integrations
"ip": "123.123.123.123", // used by external integrations
"user_agent": "Mozilla/5.0..." // used by external integrations
}
```
**It is not recommended to pass `phone_number` directly on the user when creating a cart or order, we recommend passing a local phone number in the `shipping_address` instead.** Address fields have country context, which allows local phone numbers to be passed and converted to [E.164 format](https://en.wikipedia.org/wiki/E.164) before being saved. A `phone_number` passed to the `user` object directly must be in E.164 format.
### Cart / Order Attribution Detail [#cart--order-attribution-detail]
Cart and Order `attribution` object sets the [marketing attribution](https://docs.nextcommerce.com/docs/features/offers/marketing-attribution) on the order for tracking the source of orders and use in orders reporting. You can also pass in [metadata](https://docs.nextcommerce.com/docs/build-a-store/technical-settings/metadata-fields-and-tags) fields that are configured on the store to track custom attribution parameters and integrate external tracking platforms.
```json title="Attribution Detail"
"attribution": {
"funnel": "Funnel Offer V2",
"metadata": {
"custom_meta_field": "Custom meta data"
}
}
```
### Order Shipping Detail [#order-shipping-detail]
```json title="Shipping Detail"
"shipping_code": "default",
"shipping_price": "5.48",
```
The `shipping_code` is an optional field to specify the Shipping Method to be used for the order, **if not passed, the cheapest Shipping Method will be used**. `shipping_price` is also optional and provides a way to override the configured price for the Shipping Method of the order allowing you to discount or charge an upsell for shipping on the order.
### Order Addresses Detail [#order-addresses-detail]
The `shipping_address` object on the order represents the address where the order will be shipped, and similarly for the `billing_address`. The first `shipping_address` and `billing_address` created for a user is automatically set as their default shipping and billing address, [see API Reference](/docs/admin-api/reference/orders/ordersCreate).
Use `billing_same_as_shipping_address` to forego having to pass a full duplicate address for `billing_address`.
```json title="Address Detail"
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"line1": "9975 Berkshire Dr.",
"line4": "Monsey", // city
"postcode": "10952",
"phone_number": "2025550140",
"state": "NY",
"country": "US"
},
"billing_same_as_shipping_address": true,
```
* User `phone_number` is automatically saved from the user's first address if they do not have an existing `phone_number`.
* In a "Two Step" flow where the customer address is collected before creating the order, create an address for the user and then pass the `use_default_shipping_address` and `use_default_billing_address` as true on the `orders_create` request.
### Order Payment Detail [#order-payment-detail]
The order `payment_method` and `payment_details` objects work in tandem to specify the payment method for the order and provide any additional data that may be required per payment method.
The example below uses a [Test Card Token](/docs/admin-api/guides/testing-guide) to create a **Test Order**.
```json title="Payment Detail"
"payment_method": "card_token",
"payment_details": {
"card_token": "test_card", // See iFrame Payment Form Guide
"statement_descriptor": "BRANDNAME" // See details below
},
```
To route the order to a specific gateway or gateway group, include `payment_gateway` or `payment_gateway_group` (an id, never both) inside `payment_details`. See [Gateway Routing](/docs/admin-api/guides/payment-methods/bankcard#gateway-routing) in the Bankcard guide.
#### Statement Descriptor [#statement-descriptor]
Merchants have the option to pass a custom `statement_descriptor` on orders so the end customer will more easily recognize the charge on their card bank statement. Using this field will override all subsequent transactions for the bankcard, even across payment gateways.
Descriptors can be up to 22 alphanumeric characters, spaces, and these special characters: `& , . - #`. Passing in an invalid statement descriptor will be ignored.
#### Payment Method Guides [#payment-method-guides]
See the [**Payment Methods**](/docs/admin-api/guides/payment-methods) section for method-specific guides on creating orders, plus the full capability matrix (flow type, express checkout, upsell, and subscription support per method).
**[View all payment methods →](/docs/admin-api/guides/payment-methods)**
# API Order Management (/docs/admin-api/guides/order-management)
Order management operations can be automated through the Admin API for more efficient operations and bulk actions on large quanities of orders.
Below are best practices and guides for common scenarios merchants and partners use to manage orders on the Admin API.
### Order Items Editing [#order-items-editing]
Editing items on an order is common practice, such as swapping products purchased for a different size or color with the same value without needing to collect payment or create a refund.
Order editing APIs are only available on 2024-04-01 API Version and above, if you are still using older versions we recommend you upgrade your integration.
Order editing APIs also do not affect order payment within each request. To remove items with an associated refund, see [order refunds](#order-refunds). Using order edit APIs can result in the customer owing or the merchant owing to the customer.
#### Line Item Quantities Explained [#line-item-quantities-explained]
Order line items have 4 quantity attributes that represent quantities at different states in an order life cycle.
* `quantity` - Item quantity total ever added to the order in this line.
* `current_quantity` - Current item quantity that have not yet been removed.
* `fulfillable_quantity` - Item quantity that have not yet been fulfilled, such as a partial fulfillment.
* `editable_quantity` - Item quantity that currently can be edited.
```json title="Line Item Quantities Explained"
"lines": [
{
...
"quantity": 3, // quantity of items ever added
"current_quantity": 2, // current quantity that have not been removed
"fulfillable_quantity": 1, // quantity that have not yet been fulfilled
"editable_quantity": 1, // quantity that can be edited
...
}
]
```
### Swap Items Flow [#swap-items-flow]
Swapping Items on an order is a 4 step process:
1. Retrieve order line items using the [ordersRetrieve](/docs/admin-api/reference/orders/ordersRetrieve) endpoint and check `editable_quantity` is > 0.
2. Update/Remove line items using the [ordersLinesPartialUpdate](/docs/admin-api/reference/orders/ordersLinesPartialUpdate) or [ordersLinesDestroy](/docs/admin-api/reference/orders/ordersLinesDestroy) endpoint.
3. Create new line item [ordersLinesCreate](/docs/admin-api/reference/orders/ordersLinesCreate) endpoint.
4. [Collect payment for an outstanding](#collect-payment-for-outstanding-balance) balance using the [ordersCollectPaymentCreate](/docs/admin-api/reference/orders/ordersCollectPaymentCreate) endpoint.
#### Update Existing Line Item [#update-existing-line-item]
Below is an example API call to change the quantity of a line item to 1. If the existing quantity was 2, this would remove 1 quantity, can also be used to increase line item quantity. This endpoint only accepts quantity changes, to change the product or price, see [ordersLinesCreate](/docs/admin-api/reference/orders/ordersLinesCreate) endpoint.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/orders/{number}/lines/{lineID}/"
{
"quantity": 1, // change quantity to 1
"reason": "product swap" // optional
}
```
#### Remove Full Line Item [#remove-full-line-item]
Below is an example DELETE request to the [ordersLinesDestroy](/docs/admin-api/reference/orders/ordersLinesDestroy) endpoint to remove a line item.
```json title="__http:DELETE:https://{store}.29next.store/api/admin/orders/{number}/lines/{lineID}/"
{}
```
Line items have `editable_quantity` which represents the item quantity not already in process of being fulfilled, already fulfilled, or already removed from the order.
**If `editable_quantity` is `0`, the line item cannot be edited.**
#### Create New Line Item [#create-new-line-item]
Below is an example POST request to the [ordersLinesCreate](/docs/admin-api/reference/orders/ordersLinesCreate) endpoint to create a new line item.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/lines/"
{
"product_id": 184, // product variant id
"quantity": 1,
"price": 89.99, // optional
"reason": "product swap" // optional
}
```
#### Collect Payment for Outstanding Balance [#collect-payment-for-outstanding-balance]
Orders can have an outstanding balance owed by the customer as a result of changing items on the order. To collect the outstanding balance, use the [ordersCollectPaymentCreate](/docs/admin-api/reference/orders/ordersCollectPaymentCreate) endpoint to initate a payment transaction with the order's initial payment method.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/collect-payment/"
{
"send_payment_notification": true // optionally send notificaiton to customer
}
```
### Order Refunds [#order-refunds]
Order management actions that require refunding and removing items from an order can be done through the refund flow. The refund flow is espcially useful when creating partial refunds or creating refunds for items that have already shipped to the customer.
#### Refund Flow [#refund-flow]
Refunding specific items of an order is a 3-step process:
1. Retreive order line items using the [ordersRetrieve](/docs/admin-api/reference/orders/ordersRetrieve) endpoint.
2. Calculate the refund using the [ordersRefundCalculateCreate](/docs/admin-api/reference/orders/ordersRefundCalculateCreate) endpoint.
3. Create the refund using the [ordersRefundCreate](/docs/admin-api/reference/orders/ordersRefundCreate) endpoint.
Order Refund Calculate APIs are only available on `2024-04-01` version and newer. See [API Versioning](/docs/admin-api#versioning) for how to specify a version in your requests.
#### Retrieve Order Lines [#retrieve-order-lines]
Below is an abreviated example request to [ordersRetrieve](/docs/admin-api/reference/orders/ordersRetrieve) endpoint to get the line items of the order.
```json title="__http:GET:https://{store}.29next.store/api/admin/orders/{number}/"
{
"lines": [
{
"id": 1000, // order line item
"quantity": 3, // quantity in the order
"current_quantity": 3 // quantity available and not yet removed from the order
}
]
}
```
Order payments must be captured in order to create partial refunds, uncaptured payments cannot be partially refunded.
#### Calculate Refund [#calculate-refund]
Call the [ordersRefundCalculateCreate](/docs/admin-api/reference/orders/ordersRefundCalculateCreate) endpoint with your line items to calculate the refund and see which initial payment transactions will be refunded.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/refund/calculate/"
{
"refund_lines": [
{
"line_id": 1000, // line item we want to refund
"quantity": 1 // quantity to refund
}
],
"refund_shipping": {
"full_refund": false, // set true to fully refund shipping
"amount_excl_tax": "3.99"
}
}
```
Below is the response from the [ordersRefundCalculateCreate](/docs/admin-api/reference/orders/ordersRefundCalculateCreate) API that has been slightly abbreviated to focus on the relevant refund items.
```json title="Refund Calculate Response"
{
"refund_lines": [
{
"line_id": 1000, // line item being refunded
"quantity": 1, // number of items being refunded
"restock_type": "no_restock", // restock action, see notes
"refundable_quantity": 1, // items that can be refunded
"amount_excl_tax": "59.99", // amount to be refunded excl tax
"amount_incl_tax": "59.99", // amount to be refunded incl tax
"total_tax": "0.00" // total tax on this line item to refund
}
],
"refund_shipping": {
"amount_incl_tax": "3.99", // amount to be refunded incl tax
"amount_excl_tax": "3.99", // amount to be refunded excl tax
"total_tax": "0.00", // shipping tax to be refunded
"refundable_amount": "4.99" // shipping amount avialable to refund
},
"transactions": [
{
"id": 1000, // transaction the refund will be allocated to, can be multiple.
"amount": "63.98", // amount being refunded on this transaction
"refundable_amount": "184.96" // amount available to refund with this transaction
}
],
"amount_excl_tax": "63.98", // total amoutn excl tax being refunded
"amount_incl_tax": "63.98", // total amount incl to be refunded
"total_tax": "0.00", // total tax being refunded
"currency": "USD"
}
```
#### Create Refund [#create-refund]
We're now ready to create a refund using the [ordersRefundCreate](/docs/admin-api/reference/orders/ordersRefundCreate) endpoint, see request details below.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/refund/"
{
"note": "Example reason for the refund",
"refund_lines": [
{
"line_id": 1000, // line item being refunded
"quantity": 1, // number of items being refunded
"restock_type": "no_restock", // restock action (from calculate step)
}
],
"refund_shipping": {
"amount_excl_tax": "3.99",
"full_refund": false
},
"send_refund_notification": true, // send refund email notification to customer
"transactions": [
{
"id": 1000, // transaction id to refund (from calculate step)
"amount": "63.98" // amount to refund to this transaction (from calculate step)
}
]
}
```
Depending on the product type and status of the line items being refunded, there are differen't restock actions available.
**Physical Product**
* If unfulfilled, restock\_type must be `cancel`.
* If fullfilled, restock\_type can be `return` or `no_restock`.
**Digital Product**
* If unfulfilled, restock\_type must be `cancel`.
* If fulfilled, restock\_type must be `no_restock`.
### Update Shipping Address [#update-shipping-address]
Updating an order shipping address is a common task that can be done with a PATCH request to the [ordersUpdate](/docs/admin-api/reference/orders/ordersUpdate) endpoint.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/orders/{number}/"
{
"shipping_address": {
"line1": "4765 Test Lane West", // new shipping address line 1
"line4": "Mountain Pass", // new shipping address city
"state": "CA", // new shipping address state
"postcode": "92366", // new shipping address postcode
"country": "US" // new shipping address country
}
}
```
Updating an order shipping address should be done **before** the order is sent to a fulfillment location for fulfillment. If the order has already been accepted and processing, send a a [cancellationRequestSend](/docs/admin-api/reference/fulfillment/cancellationRequestSend) request and then [fulfillmentRequestSend](/docs/admin-api/reference/fulfillment/fulfillmentRequestSend) after you've updated the shipping address.
### Request Fulfillment [#request-fulfillment]
Fulfillment can be requested immediately through the [fulfillmentRequestSend](/docs/admin-api/reference/fulfillment/fulfillmentRequestSend) for cases that you'd like to immediately send the fulfillment order to the fulfillment location for fulfillment.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillment-request/"
{
"fulfillment_order_line_items": [ // will split the fulfillment order into a new fulfillment order
{
"id": 0,
"quantity": 1
}
],
"message": "Special message to warehouse", // message to the fulfillment location
"notify": true // notify the customer when fulfillment order is fulfilled
}
```
### Hold Fulfillment [#hold-fulfillment]
Holding fulfillment for an order while waiting for additional review or making adjustments to the order before sending to the fulfillment location for shipping.
1. Retrieve all fulfillment Orders using the [ordersFulfillmentOrdersRetrieve](/docs/admin-api/reference/orders/ordersFulfillmentOrdersRetrieve) endpoint.
2. Send a [fulfillmentOrdersHold](/docs/admin-api/reference/fulfillment/fulfillmentOrdersHold) request for each fulfillment order to hold.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/hold/"
{
"reason": "address_incorrect", // see available reasons in api reference
"reason_message": "Additional relevant detail." // provide additional relevant detail
}
```
### Cancel Fulfillment [#cancel-fulfillment]
Canceling a fulfillment order that is already accepted and processing with a fulfillment location is a common order management task to stop fulfillment or as a prerequisite step to moving a fulfillment order to a new location.
1. Retrieve all fulfillment Orders using the [ordersFulfillmentOrdersRetrieve](/docs/admin-api/reference/orders/ordersFulfillmentOrdersRetrieve) endpoint.
2. Send a [cancellationRequestSend](/docs/admin-api/reference/fulfillment/cancellationRequestSend) request for each fulfillment order to request fulfillment cancellation.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/cancellation-request/"
{
"message": "Reason why canceling fulfillment" // message sent to the fulfillment location
}
```
### Move Fulfillment Orders [#move-fulfillment-orders]
### Add Fulfillment Tracking [#add-fulfillment-tracking]
Adding tracking information to a fulfillment order marks it as fulfilled and optionally notifies the customer with shipment tracking details. Use the [fulfillmentsCreate](/docs/admin-api/reference/fulfillment/fulfillmentsCreate) endpoint to create a fulfillment with tracking info.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillments/"
{
"notify": true, // send the customer an order shipped notification email
"tracking_info": [
{
"tracking_code": "EXAMPLECODE",
"carrier": "dhl_ecommerce"
}
]
}
```
The `tracking_info` field accepts an array, allowing you to add multiple tracking numbers for a single fulfillment order when a shipment is split across multiple packages.
### Cancel Order [#cancel-order]
Canceling an order is a common order management task when you need to cancel the entire order and refund all payment transactions. To cancel an order, send a request to the [ordersCancelCreate](/docs/admin-api/reference/orders/ordersCancelCreate) endpoint.
```json title="__http:POST:https://{store}.29next.store/api/admin/orders/{number}/cancel/"
{
"cancel_reason": "Customer wants to cancel", // Appropiate cancel reason message
"full_refund": true, // Refund all remaining payments or not
"send_cancel_notification": true // Send the customer a notification or not
}
```
# API Subscription Management (/docs/admin-api/guides/subscription-management)
Subscriptions management can be done through Admin API to automate business processes and perform bulk operations.
Subscription management actions most often times will only affect future renewals orders/charges of the subscription.
### Create Subscription [#create-subscription]
Subscriptions can be created directly through the [subscriptionsCreate](/docs/admin-api/reference/subscriptions/subscriptionsCreate) Admin API endpoint for scenarios such as custom order flows or importing subcriptions from another platform.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/"
{
"lines": [ // subscription line item products and pricing
{
"product_id": 123,
"quantity": 1,
"currency": "USD",
"price": "19.99"
}
],
"shipping_code": "default-shipping", // shipping method for renewal orders
"shipping_price": "3.99", // shipping price for renewal orders
"interval": "day", // subscription renewal interval
"interval_count": 30, // subscription renewal interval count
"next_renewal_date": "2022-2-28T08:41:37+07:00", // first renewal date
"user": {
"email": "john@smiths.com", // get or create a customer based on email address
"first_name": "John",
"last_name": "Smith"
},
"use_default_billing_address": true, // alternatively pass full billing_address
"use_default_shipping_address": true, // alternatively pass full shipping_address
"payment_method": "card_token",
"payment_details": {
"card_token": "" // see card tokenization iFrame guide
},
}
```
### Updating Products & Pricing [#updating-products--pricing]
Updating subscription recurring items and pricing can be done through [subscriptionLinesCreate](/docs/admin-api/reference/subscriptions/subscriptionsLinesCreate), [subscriptionLinesUpdate](/docs/admin-api/reference/subscriptions/subscriptionsLinesUpdate), and [subscriptionsLinesDestroy](/docs/admin-api/reference/subscriptions/subscriptionsLinesDestroy) endpoints.
**Adding an Additional Product**
To add a new product to a subscription, use the [subscriptionLinesCreate](/docs/admin-api/reference/subscriptions/subscriptionsLinesCreate) Admin API endpoint with the product, price, and quanity details.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/lines/"
{
"price": "9.99", // recurring price for the product
"product_id": 100, // product ID
"quantity": 1 // quantity of the product
}
```
**Updating an Existing Line Item Product Price**
To update and existing product price and quantity on a subscription line, use the [subscriptionsLinesUpdate](/docs/admin-api/reference/subscriptions/subscriptionsLinesUpdate) Admin API endpoint with the new price, and new quanity details.
```json title="__http:PUT:https://{store}.29next.store/api/admin/subscriptions/{id}/lines/{lineId}/"
{
"price": "9.99", // new recurring price for the product
"quantity": 1 // new quantity of the product
}
```
**Removing a Product**
To remove a product from a subscription, send a DELETE request to the [subscriptionsLinesDestroy](/docs/admin-api/reference/subscriptions/subscriptionsLinesDestroy) endpoint to remove the line item (ie the product) from future renewal orders created from the subscription.
```json title="__http:DELETE:https://{store}.29next.store/api/admin/subscriptions/{id}/lines/{lineId}/"
{}
```
Subscriptions must have at least one line item with a product, you can alternatively cancel the subscription to stop all future renewals.
### Updating Renewal Schedule [#updating-renewal-schedule]
Changing the renewal schedule of a subscription can be achived with a PATCH request to the [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) endpoint with a new `interval` and `interval_count`, ie 30 days.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/subscriptions/{id}/"
{
"interval": "day",
"interval_count": 30,
"next_renewal_date": "2025-06-29T03:28:59.193252-05:00" // optional next renewal date
}
```
### Changing Next Renewal Date [#changing-next-renewal-date]
Changing the next renewal date of a subscription can be achieved through updating the `next_renewal_date` key on the subscription object with a PATCH request to the [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) endpoint with your new renewal date and time.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/subscriptions/{id}/"
{
"next_renewal_date": "2025-06-29T03:28:59.193252-05:00" // next renewal date & time
}
```
If you would like to immediately renew the subscription, you can pass a date from the past and the subscription will process a renewal attempt within the next 30 minutes.
### Triggering a Renewal [#triggering-a-renewal]
To immediately trigger a renewal order for an active subscription, use the [subscriptionsRenewCreate](/docs/admin-api/reference/subscriptions/subscriptionsRenewCreate) endpoint. This creates a new renewal order on demand without waiting for the next scheduled renewal date.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/renew/"
{}
```
The endpoint returns the full subscription object with updated renewal details on success.
The subscription must be in an `active` status to trigger a renewal. For subscriptions in `past_due` status, use the [subscriptionsRetryCreate](/docs/admin-api/reference/subscriptions/subscriptionsRetryCreate) endpoint instead.
### Identifying Subscription Charges [#identifying-subscription-charges]
When a `transaction.created` webhook is associated with a subscription, `data.subscription` contains the subscription ID and billing cycle:
```json title="Subscription Transaction"
{
"event_type": "transaction.created",
"data": {
"id": 10416,
"subscription": {
"id": 12345,
"billing_cycle": 3
}
}
}
```
* `billing_cycle: 0` identifies the initial subscription charge.
* `billing_cycle: 1` identifies the first renewal.
* Each subsequent renewal increments the value by one.
The subscription object is empty when the transaction is not associated with a subscription.
### Updating Payment Details [#updating-payment-details]
Updating the Payment Gateway of a subscription can done through the [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) endpoint.
**Changing Payment Gateway**
To change the payment gateway used for bankcard payments of a subscription, send a PATCH request to the [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) endpoint with the new `payment_gateway`.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/subscriptions/{id}/"
{
"payment_details": {
"payment_gateway": 23 // new payment gateway to be used on the next renewal
}
}
```
**Updating Bankcard Payment Method**
To change the bankcard on a subscription, pass a new `card_token` with a PATCH request to the [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) endpoint.
To update a bankcard on a subscription, you must use the **[iFrame to tokenize](/docs/admin-api/guides/payment-methods/bankcard)** the bankcard and use the `card_token` when updating the subscription itself.
The new bankcard will be automatically verfied with a `verify` request to a payment gateway to ensure the new bankcard is valid and can be used for future renewals. If the bankcard cannot be verified, the update request will fail and return an error.
**New Bankcard Payment Method Flow**
```json title="__http:PATCH:https://{store}.29next.store/api/admin/subscriptions/{id}/"
{
"payment_details": {
"card_token": "", // new card token
"payment_gateway": 23 // optionally pass a specific gateway
},
"billing_address": {
"country": "US", // optionally pass a new billing address
"first_name": "John",
"last_name": "Doe",
"line1": "123 East West St.",
"line4": "New York",
"state": "NY",
"postcode": "90210"
}
}
```
### Retrying Renewal [#retrying-renewal]
Subscriptions that are `past_due` status can be attemted to retry the renewal, often combined with a new `payment_gateway`, by using the [subscriptionsRetryCreate](/docs/admin-api/reference/subscriptions/subscriptionsRetryCreate) endpoint.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/retry/"
{
"payment_gateway": 122 // optional new payment gateway to retry with
}
```
The subscription retry endpoint is useful for custom recovery logic when attempting to recover failing subscriptions.
### Pause [#pause]
To temporarily stop renewals on an active subscription without cancelling it, use the [subscriptionsPauseCreate](/docs/admin-api/reference/subscriptions/subscriptionsPauseCreate) endpoint. Pausing is useful for win-back flows, customer-requested holds, or pausing a cohort during inventory or fulfillment issues.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/pause/"
{
"pause_until": "2026-08-01" // optional date (YYYY-MM-DD) to auto-resume on
}
```
If `pause_until` is omitted the subscription is paused indefinitely and will be auto-cancelled if it is not resumed within 6 months. The endpoint returns the full subscription object with status `paused` and the `paused_at` / `paused_until` timestamps populated.
Paused subscriptions skip all scheduled renewals until the `pause_until` date is reached, at which point renewals resume automatically on the existing schedule.
To resume a paused subscription before its `pause_until` date — or to reactivate an indefinitely paused subscription — use the [subscriptionsResumeCreate](/docs/admin-api/reference/subscriptions/subscriptionsResumeCreate) endpoint with a future `next_renewal_date`.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/resume/"
{
"next_renewal_date": "2026-06-15" // required, must be a future date
}
```
### Cancel [#cancel]
To cancel a subscription, use the [subscriptionsCancelCreate](/docs/admin-api/reference/subscriptions/subscriptionsCancelCreate) endpoint to stop all future renewals.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/cancel/"
{
"cancel_reason": "not_satisfied_with_product", // required
"cancel_reason_other_message": "Wasn't happy with result", // optional
"send_cancel_notification": true // to send the cancelation email or not
}
```
### Bulk Subscription Actions [#bulk-subscription-actions]
The [`/next-bulk-subscription`](https://github.com/NextCommerceCo/skills/tree/main/next-bulk-subscription) skill in the Next Commerce AI skills repo wraps this workflow — CSV ingestion, dry-run validation, rate limiting, and results reporting for bulk pauses, cancellations, renewal-date shifts, and other subscription updates — for Claude Code, Cursor, and other AI coding agents.
For operations that affect a cohort of subscriptions, iterate the endpoint that matches the subscription action: use [subscriptionsPauseCreate](/docs/admin-api/reference/subscriptions/subscriptionsPauseCreate) for pauses, [subscriptionsCancelCreate](/docs/admin-api/reference/subscriptions/subscriptionsCancelCreate) for cancellations, and [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate) for field updates such as renewal dates, cadence, addresses, or payment gateway details. Respect the **4 requests/second rate limit** (sleep \~0.26s between calls) and write each response to a log file so failures can be retried without re-running the whole batch.
**Bulk pause.** Loop [subscriptionsPauseCreate](/docs/admin-api/reference/subscriptions/subscriptionsPauseCreate) across the list, passing the same `pause_until` date for the cohort, a per-row `pause_until` from your input file, or `{}` for indefinite pauses.
```json title="__http:POST:https://{store}.29next.store/api/admin/subscriptions/{id}/pause/"
{
"pause_until": "2026-08-01"
}
```
Do not bulk pause by PATCHing `status: "paused"`. Use the pause endpoint so the platform records the pause lifecycle fields and applies pause behavior consistently.
**Bulk renewal-date shift.** Update each subscription's `next_renewal_date` to defer or align upcoming charges across a cohort.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/subscriptions/{id}/"
{
"next_renewal_date": "2026-08-17T10:09:01-04:00"
}
```
When computing the new date, preserve the store-local timezone offset returned by the API (e.g., `-04:00`). The platform evaluates renewals in store-local time, so avoid converting to UTC.
**Bulk cancellation.** Loop [subscriptionsCancelCreate](/docs/admin-api/reference/subscriptions/subscriptionsCancelCreate) across the list, passing the same `cancel_reason` for the cohort. Suppress the customer email with `send_cancel_notification: false` if you're handling communications separately.
**Bulk gateway migration.** When a gateway is being retired, iterate PATCH calls that update `payment_details.gateway.id` on each affected bankcard subscription.
# Test Order Flows (/docs/admin-api/guides/testing-guide)
Testing your integration is a critical step when developing on the Next Commerce platform. There are two distinct paths to creating Test Orders, Transactions, and Subscriptions through the Admin API or storefront checkout flow.
Cards must be tokenized before submitting on the Admin API, see [iframe card tokenization](/docs/admin-api/guides/payment-methods/bankcard) guide.
### Test Cards [#test-cards]
Test cards can be used on live stores and live integrations to create **Test Orders** with the exception they do not touch the gateway and have no attached transactions.
| Test Card Number | Expiration | CVV | Use Case |
| ---------------- | --------------- | --- | ---------------------------------------------- |
| 6011111111111117 | Any Future Date | Any | Test payment success flow without transaction. |
| 6011000990139424 | Any Future Date | Any | Test 3DS payment flow without transaction. |
Test cards can be used to test your live integration flows without changing any store payment settings. **Test cards are generally safe to use to test your flows.**
### Test Card Tokens [#test-card-tokens]
Test card tokens can be used on the API directly without needing to tokenize the card before submitting the create order request.
| Test Card Number | Use Case |
| ---------------- | ---------------------------------------------- |
| `test_card` | Test payment success flow without transaction. |
| `test_3ds_card` | Test 3DS payment flow without transaction. |
Use the test card tokens before you've integrated the [iFrame Card Tokenization](/docs/admin-api/guides/payment-methods/bankcard) to validate your API requests.
### Test Gateway [#test-gateway]
The Test Gateway behaves exactly as a regular gateway, when orders are created using the `test` gateway, they also have associated Test transactions.
| Test Card Number | Expiration | CVV | Use Case |
| ---------------- | --------------- | --- | ------------------------------------------------------------ |
| 4111111111111111 | Any Future Date | Any | Test standard payment flow with successful transaction. |
| 5555555555554444 | Any Future Date | Any | Test 3DS payment flow with successful transaction. |
| 4012888888881881 | Any Future Date | Any | Test standard payment declined flow with failed transaction. |
#### Setup Test Gateway [#setup-test-gateway]
To setup the test gateway, go to **Settings > Payments > Add Gateway** to add the Test Gateway to your store. Next, add the Test Gateway to your default gateway group or use the gateway ID directly through the Admin API.
The `test` gateway path requires setting up the gateway and can negatively impact your store's live order flows. **Use with caution if your store has live traffic.**
### Test Subscriptions [#test-subscriptions]
Subscriptions can be created with both the [Test Gateway](#test-gateway) and [Test Cards](#test-cards), however there are some small behavior differences at this time.
| Test Card Number | Expiration | CVV | Use Case |
| ---------------- | --------------- | --- | --------------------------------------------------- |
| 4111111111111111 | Any Future Date | Any | Test subscription can create renewal orders. |
| 6011111111111117 | Any Future Date | Any | Test subscription **cannot** create renewal orders. |
# Campaigns Admin API (/docs/campaigns/admin-api)
The Admin API gives you full setup and management of campaigns without going through the dashboard. Create a campaign, add its packages, offers, and shipping methods, read back the API key your funnel pages need, and update any of it later as your catalogue and pricing change. Everything is available over the API, so an AI agent can drive the entire campaign lifecycle for you.
It also pays off once you run more than a handful of campaigns:
* Stand up a campaign per market or per test, each with its own currency and payment methods
* Update every campaign that sells a product after you reprice or discontinue it
* Push a new price to one currency and let the rest recalculate from your default
Admin API tokens have full access to your store, so they belong on a server. Your funnel pages call the [Campaign Cart API](/docs/campaigns/api) instead, on a different host with a per-campaign key. That key is the only credential safe to ship in browser code.
## Permissions [#permissions]
Campaign endpoints are authorized by OAuth scope on the app making the request.
| Scope | Grants |
| ----------------- | ------------------------------------------------- |
| `campaigns:read` | List and view campaigns and their packages |
| `campaigns:write` | Create, update, and delete campaigns and packages |
Additional scopes required:
| Scope | Needed to |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `catalogue:read` | Find the `product_id` and `product_variant_ids` a package links to, using [productsList](/docs/admin-api/reference/products/productsList) and [productsVariantList](/docs/admin-api/reference/products/productsVariantList) |
| `gateways:read` | Read gateway groups and the payment method codes a campaign can enable, using [gatewayGroupsList](/docs/admin-api/reference/payments/gatewayGroupsList) |
See [Admin API permissions](/docs/admin-api/permissions) for the full scope list and [Admin API → Getting Started](/docs/admin-api) for creating an OAuth app.
Campaign endpoints exist on the `2024-04-01` API version and above. They are not available on `2023-02-10`.
## Campaign Provisioning Flow [#campaign-provisioning-flow]
Standing up a campaign end to end is a 4 step process:
1. Retrieve the payment gateway group using the [gatewayGroupsList](/docs/admin-api/reference/payments/gatewayGroupsList) endpoint to get its `id` and the payment methods it supports.
2. Create the campaign using the [campaignsCreate](/docs/admin-api/reference/campaigns/campaignsCreate) endpoint.
3. Add a package for each sellable item using the [campaignsPackagesCreate](/docs/admin-api/reference/campaigns/campaignsPackagesCreate) endpoint.
4. Read the campaign's `api_key` from the create response and configure your funnel pages with it.
### Create a Campaign [#create-a-campaign]
Below is an example API call to create a campaign for a US English funnel that also accepts CAD. `name`, `currency`, `language`, and `payment_gateway_group_id` are required, everything else is optional.
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/"
{
"name": "Spring Widget Launch US", // internal reference name
"currency": "USD", // default currency for orders on this campaign
"language": "en", // preferred customer language
"payment_gateway_group_id": 4, // from gatewayGroupsList
"additional_currencies": ["CAD"], // currencies supported beyond the default
"available_payment_methods": ["card_token", "paypal"], // codes from gatewayGroupsList
"available_express_payment_methods": ["apple_pay"], // express codes from gatewayGroupsList
"available_shipping_countries": ["US", "CA"], // ISO 3166-1 alpha-2 codes
"statement_descriptor": "WIDGETCO" // shown on the customer's card statement
}
```
The response returns the campaign `id` you use for every package call, and the `api_key` your funnel pages authenticate with.
The payment methods you enable have to be ones the gateway group supports. [gatewayGroupsList](/docs/admin-api/reference/payments/gatewayGroupsList) returns each group on the store with its `id`, `available_payment_methods`, `available_express_payment_methods`, and `available_currencies`.
### Add Packages to a Campaign [#add-packages-to-a-campaign]
A package is the campaign's sellable reference to a product or variant, and its `id` is what your funnel markup passes as `data-next-package-id`. Only `name` and `product_id` are required.
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/{id}/packages/"
{
"name": "Widget Single", // package name
"product_id": 184, // product linked to the package
"product_variant_ids": [512], // product variants linked to the package
"price": "49.95" // price per product variant unit
}
```
Avoid creating `1x`, `2x`, and `3x` package records. Create one package for the product identity and base price, then discount quantity tiers with offers using the [campaignsOffersCreate](/docs/admin-api/reference/campaigns/campaignsOffersCreate) endpoint. The cart and order APIs return before and after totals, so your pages render savings without doing math. See [Concepts → Offers](/docs/campaigns#offers).
### Create a Subscription Package [#create-a-subscription-package]
Subscription packages carry a recurring interval and a separate recurring price. `interval` accepts `day` or `month`.
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/{id}/packages/"
{
"name": "Widget Monthly Refill",
"product_id": 184,
"product_variant_ids": [512],
"price": "49.95", // charged on the initial order
"price_recurring": "39.95", // charged on each renewal
"interval": "month", // day or month
"interval_count": 1 // renew every 1 month
}
```
### Set a Package Image [#set-a-package-image]
A package starts with its product's catalogue image. To show a campaign specific image instead, [campaignsPackagesImageUpdate](/docs/admin-api/reference/campaigns/campaignsPackagesImageUpdate) replaces it from either a URL (`src`) or base64 data (`attachment`). Send one or the other, not both.
```json title="__http:PUT:https://{store}.29next.store/api/admin/campaigns/{id}/packages/{packageId}/image/"
{
"src": "https://cdn.example.com/widget-single-hero.png", // image URL to fetch
"file_name": "widget-single-hero" // optional, the extension is taken from the image
}
```
The response is the full package with the new `image` URL. Accepted formats are JPG, PNG, ICO, GIF, and WebP, up to 10 MB and 25 megapixels. [campaignsPackagesImageDestroy](/docs/admin-api/reference/campaigns/campaignsPackagesImageDestroy) removes the image from the package. See [Update a Campaign Package Image](/docs/admin-api/guides/campaign-package-images) for the base64 upload variant and the full flow.
The package `image` returned by the [Campaign Cart API](/docs/campaigns/api) comes from this setting, and cart and order lines carry it as `package_image`. Pages that render package images pick up the change without a deploy.
### Repricing Across Currencies [#repricing-across-currencies]
[campaignsPackagesCreate](/docs/admin-api/reference/campaigns/campaignsPackagesCreate) takes a single `price` and `price_recurring`. [campaignsPackagesPartialUpdate](/docs/admin-api/reference/campaigns/campaignsPackagesPartialUpdate) takes a `prices` array with one entry per currency, and currencies you leave out are not changed.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/campaigns/{id}/packages/{packageId}/"
{
"prices": [
{
"currency": "USD",
"price": "44.95",
"price_recurring": "34.95"
}
],
"recalculate_prices": true // derive the other currencies from the default currency price
}
```
With `recalculate_prices` set to `true`, every currency you did not list is recalculated from the campaign's default currency price. Left `false` (the default), only the currencies you passed change.
`price` and `price_recurring` are write-only and do not appear in the response. Read the resolved figures from the response's `prices` array, which returns `currency`, `price`, and `price_recurring` for each. You also send `product_variant_ids` as an array and read back a single `product_variant_id`.
### Add Offers to a Campaign [#add-offers-to-a-campaign]
An offer discounts the order once its condition is met. `name`, `condition`, and `benefit` are required. The condition decides which packages and what quantity trigger the offer, and the benefit sets the percentage off. An offer applies automatically at checkout unless it is a code offer (`offer_type` of `voucher`).
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/{id}/offers/"
{
"name": "Buy 2 Save 30%", // unique within the campaign
"condition": {
"type": "count", // any, or count for a minimum quantity
"value": 2, // minimum package quantity, required when type is count
"package_ids": [2231] // package ids from campaignsPackagesCreate, or send all_packages: true
},
"benefit": {
"type": "package_percentage", // package_percentage, shipping_percentage, or order_percentage
"value": "30.00", // percentage off
"price_rounding": "0.95" // round each discounted unit price to XX.95, omit for no rounding
}
}
```
Create one offer per quantity tier to control the price the customer pays at each quantity. See [Concepts → Offers](/docs/campaigns#offers) for a worked example.
Use a code offer for discounts on upsell, downsell, and exit-pop pages. Automatic offers run only at checkout, so a code offer is the only way to discount an upsell or an exit-pop incentive. Set `offer_type` to `voucher` to create one. The code offer applies when the customer enters its `code`, or when your page submits the code through the `vouchers` array on the cart, order, or upsell-create request (see [Campaigns API → Offers](/docs/campaigns/api#offers)). Set `all_packages` to `true` with an `any` condition to make it valid on anything in the cart.
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/{id}/offers/"
{
"name": "Upsell 10% Off",
"offer_type": "voucher", // offer (automatic) or voucher (code offer, code required)
"code": "UPSELL10", // required when offer_type is voucher, submit it from the upsell or exit-pop page
"condition": {
"type": "any", // no quantity requirement
"all_packages": true // apply to all current and future packages
},
"benefit": {
"type": "order_percentage",
"value": "10.00"
}
}
```
`package_ids` is write-only. The response returns a `packages` array with each package's `id`, `name`, `product_id`, and `product_sku`, plus a read-only `description` on both the condition and the benefit, and an `available` flag.
### Add Shipping Methods to a Campaign [#add-shipping-methods-to-a-campaign]
A campaign shipping method links a shipping method already configured on the store to this campaign at a campaign specific price. Both `shipping_method` and `price` are required. `name` is populated automatically from the code you pass.
```json title="__http:POST:https://{store}.29next.store/api/admin/campaigns/{id}/shipping-methods/"
{
"shipping_method": "default-shipping", // code from the store's configured shipping methods
"price": "4.95" // price in the campaign's default currency
}
```
Every other currency configured on the campaign is populated automatically through forex conversion.
To change prices later, [campaignsShippingMethodsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) takes the same `prices` array and `recalculate_prices` flag as packages, and currencies you leave out are not changed.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/"
{
"prices": [
{ "currency": "USD", "price": "5.95" }
],
"recalculate_prices": true // convert the other currencies from the price above
}
```
With `recalculate_prices` set to `true`, the source is the campaign's default currency when you include it in `prices`, otherwise the first currency listed. Include the default currency or submit a single price so the source is unambiguous.
### Find Campaigns to Act On [#find-campaigns-to-act-on]
[campaignsList](/docs/admin-api/reference/campaigns/campaignsList) is cursor paginated and filterable. Follow `next` in the response until it is `null`.
| Filter | Matches |
| --------------------------------------- | ---------------------------------------------------- |
| `name` | Campaign name contains this text, case-insensitive |
| `currency` | Default currency, as an ISO 4217 code |
| `language` | Campaign language, as an ISO 639-1 code |
| `created_date_from` / `created_date_to` | Created within a date range (`YYYY-MM-DD`, UTC) |
| `updated_date_from` / `updated_date_to` | Last updated within a date range (`YYYY-MM-DD`, UTC) |
```json title="__http:GET:https://{store}.29next.store/api/admin/campaigns/?currency=EUR&updated_date_from=2026-08-01&page_size=50"
{}
```
### Audit Packages Against Your Catalogue [#audit-packages-against-your-catalogue]
[campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) filters on `name`, `product_name`, and `product_sku`, so you can find every package on a campaign that sells a discontinued SKU.
```json title="__http:GET:https://{store}.29next.store/api/admin/campaigns/{id}/packages/?product_sku=WIDGET-BLU"
{}
```
Each package returns `product_purchase_availability` (`available` or `unavailable`) and `product_inventory_availability` (`in_stock`, `low_stock`, `out_of_stock`, or `untracked`). Check these to catch packages pointing at products customers can no longer buy.
Unlike [campaignsList](/docs/admin-api/reference/campaigns/campaignsList), [campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) and [campaignsShippingMethodsList](/docs/admin-api/reference/campaigns/campaignsShippingMethodsList) return plain arrays with no `cursor` or `page_size`. Each returns everything on the campaign in one response.
### Retire a Campaign or Package [#retire-a-campaign-or-package]
[campaignsDestroy](/docs/admin-api/reference/campaigns/campaignsDestroy) deletes a campaign's settings and [campaignsPackagesDestroy](/docs/admin-api/reference/campaigns/campaignsPackagesDestroy) removes a single package.
A live funnel is configured with its campaign's `api_key` and its package IDs. Deleting either removes what those pages depend on, so take the pages down or repoint them at a replacement first.
Authorized domains are not managed through this API. Configure them in your store's Campaign Settings, see [Concepts → Domains](/docs/campaigns#domains).
## Endpoints [#endpoints]
Full request and response detail lives in the Admin API reference.
### Campaigns [#campaigns]
| Operation | Endpoint |
| -------------------------------------------------------------------------------------- | ----------------------------------- |
| [Campaigns List](/docs/admin-api/reference/campaigns/campaignsList) | `GET /api/admin/campaigns/` |
| [Campaigns Create](/docs/admin-api/reference/campaigns/campaignsCreate) | `POST /api/admin/campaigns/` |
| [Campaigns Retrieve](/docs/admin-api/reference/campaigns/campaignsRetrieve) | `GET /api/admin/campaigns/{id}/` |
| [Campaigns Partial Update](/docs/admin-api/reference/campaigns/campaignsPartialUpdate) | `PATCH /api/admin/campaigns/{id}/` |
| [Campaigns Destroy](/docs/admin-api/reference/campaigns/campaignsDestroy) | `DELETE /api/admin/campaigns/{id}/` |
### Offers [#offers]
| Operation | Endpoint |
| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [Campaigns Offers List](/docs/admin-api/reference/campaigns/campaignsOffersList) | `GET /api/admin/campaigns/{id}/offers/` |
| [Campaigns Offers Create](/docs/admin-api/reference/campaigns/campaignsOffersCreate) | `POST /api/admin/campaigns/{id}/offers/` |
| [Campaigns Offers Retrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) | `GET /api/admin/campaigns/{id}/offers/{offerId}/` |
| [Campaigns Offers Partial Update](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate) | `PATCH /api/admin/campaigns/{id}/offers/{offerId}/` |
| [Campaigns Offers Destroy](/docs/admin-api/reference/campaigns/campaignsOffersDestroy) | `DELETE /api/admin/campaigns/{id}/offers/{offerId}/` |
### Packages [#packages]
| Operation | Endpoint |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [Campaigns Packages List](/docs/admin-api/reference/campaigns/campaignsPackagesList) | `GET /api/admin/campaigns/{id}/packages/` |
| [Campaigns Packages Create](/docs/admin-api/reference/campaigns/campaignsPackagesCreate) | `POST /api/admin/campaigns/{id}/packages/` |
| [Campaigns Packages Retrieve](/docs/admin-api/reference/campaigns/campaignsPackagesRetrieve) | `GET /api/admin/campaigns/{id}/packages/{packageId}/` |
| [Campaigns Packages Partial Update](/docs/admin-api/reference/campaigns/campaignsPackagesPartialUpdate) | `PATCH /api/admin/campaigns/{id}/packages/{packageId}/` |
| [Campaigns Packages Destroy](/docs/admin-api/reference/campaigns/campaignsPackagesDestroy) | `DELETE /api/admin/campaigns/{id}/packages/{packageId}/` |
| [Campaigns Packages Image Update](/docs/admin-api/reference/campaigns/campaignsPackagesImageUpdate) | `PUT /api/admin/campaigns/{id}/packages/{packageId}/image/` |
| [Campaigns Packages Image Destroy](/docs/admin-api/reference/campaigns/campaignsPackagesImageDestroy) | `DELETE /api/admin/campaigns/{id}/packages/{packageId}/image/` |
### Shipping Methods [#shipping-methods]
| Operation | Endpoint |
| ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| [Campaigns Shipping Methods List](/docs/admin-api/reference/campaigns/campaignsShippingMethodsList) | `GET /api/admin/campaigns/{id}/shipping-methods/` |
| [Campaigns Shipping Methods Create](/docs/admin-api/reference/campaigns/campaignsShippingMethodsCreate) | `POST /api/admin/campaigns/{id}/shipping-methods/` |
| [Campaigns Shipping Methods Retrieve](/docs/admin-api/reference/campaigns/campaignsShippingMethodsRetrieve) | `GET /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` |
| [Campaigns Shipping Methods Partial Update](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) | `PATCH /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` |
| [Campaigns Shipping Methods Destroy](/docs/admin-api/reference/campaigns/campaignsShippingMethodsDestroy) | `DELETE /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` |
# Campaign Cart API (/docs/campaigns/api)
Campaigns App opens up new possibilities for frontend developers to easily create complex external campaign flows using JavaScript, **no backend server-side integration required**.
The Campaigns App provides an easy-to-use CORS-enabled API that follows the best practices for integrating to our Admin API for an [External Checkout Flow](/docs/admin-api/guides/external-checkout).
For most projects, start with the **[Campaigns Getting Started guide](/docs/campaigns)** — it walks you through scaffolding a working funnel with Page Kit and the Campaign Cart SDK, which wraps every endpoint on this page behind data-attribute driven add-to-cart, live cart state, coupon handling, checkout forms, and upsell flows. You write HTML, not fetch calls. The [Campaign Cart SDK documentation](https://cart-sdk.nextcommerce.com/latest/) covers the SDK in depth.
Use the raw API on this page only for custom server-side flows or backend integrations the SDK doesn't cover.
This page covers the browser-facing API your funnel pages call at runtime, authenticated with a **Campaign API key**. To create campaigns and manage packages from a backend, use the [Campaigns Admin API](/docs/campaigns/admin-api) — a different host, authenticated with an OAuth token.
## Campaigns Overview [#campaigns-overview]
A "campaign" is a defined set of packages, offers, shipping options, payment rules, and localization settings that backs the HTML/JS pages of a campaign funnel. You can set up multiple campaigns for different product offers, markets, and A/B tests.
### Session Tracking [#session-tracking]
Session tracking is available to enable visibility into campaign performance by adding a [javascript snippet](#add-session-tracking) to the head of every page on your campaign. With session tracking configured, we'll automatically track events for:
* Page View
* Cart Create
* Order Create
* Upsell Create
These events flow into Campaign Performance reports for real-time monitoring of activity on your campaign.
### Packages [#packages]
A **package** is a virtual link to a product or product variant in your campaign — what customers reference by `package_id` when creating carts and orders. In SDK 0.4.x-compatible builds, create packages for product or variant identity and send the selected quantity from the page. Older campaigns may still expose package-level quantity for compatibility, but new quantity tiers should be modeled with offers rather than separate `1x`, `2x`, and `3x` package records.
Pricing adjustments — per-quantity bundle discounts, automatic offers, coupon codes — are controlled by **offers**, not by per-package pricing rules. The cart and order APIs return adjusted before/after totals so the frontend can render savings without doing math.
See [Concepts → Packages](/docs/campaigns#packages) and [Concepts → Offers](/docs/campaigns#offers) for the full model.
### Shipping Options [#shipping-options]
Campaigns can have custom shipping prices to optimize shipping fees and methods available on your campaign to override the default pricing configured globally in the store.
**Example Shipping Methods**
* Shipping Method 1 - Default shipping at 7.99
* Shipping Method 2 - Express shipping at 14.99
### Offers [#offers]
**Offers** are the pricing layer. They apply discounts to the cart when conditions are met — either **automatically** (when the cart line/quantity matches an offer's rule) or via a **coupon code** the customer enters.
Coupon codes are submitted through the `vouchers` array on cart, order, and upsell-create requests. Codes stack on top of automatic offers, and they're the only way to apply discounts on upsell pages (automatic offers don't run there).
The cart and order responses include applied `discounts`, `total_discounts`, and per-line discount breakdowns so the frontend can render savings without recalculating. The full set of offers configured on a campaign is returned by [`campaignRetrieve`](/docs/campaigns/api/campaigns/campaignRetrieve).
See [Concepts → Offers](/docs/campaigns#offers) for the full model.
## Getting Started [#getting-started]
To get started, create a new campaign with a package mapped to a product in your store. Use the examples below with your **Campaign API Key** to get started using the Campaign Cart API.
### Add Session Tracking [#add-session-tracking]
Add the script below to every page of your campaign for full session tracking integration.
```javascript
```
### Calculate Cart [#calculate-cart]
Show customers a live pricing preview — subtotal, discounts, shipping, total — **before** they commit to creating an order. Use this on checkout pages to update totals as the user changes quantity, swaps packages, or enters a coupon code. No cart is persisted; the endpoint accepts the same `lines`/`vouchers` shape as cart create and returns adjusted totals plus a breakdown of applied offer and voucher discounts.
```javascript title="Calculate Cart Totals"
var payload = {
"lines": [
{ "package_id": 1 }
],
"vouchers": ["SAVE10"], // optional coupon codes
"shipping_method": 1 // optional — include to factor shipping into the total
}
const response = await fetch('https://campaigns.apps.29next.com/api/v1/carts/calculate/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
},
body: JSON.stringify(payload),
});
const result = await response.json();
console.log(result); // subtotal, total, total_discount, offer_discounts, voucher_discounts, lines
```
Add `?upsell=true` to the URL when calling from an upsell page — this skips site-wide automatic offers (which don't apply post-purchase). Coupon codes passed via `vouchers` still apply.
### Create Cart [#create-cart]
Capture customer details (email, name) alongside their selected packages and persist the cart server-side — so the visitor is recorded as a **lead** even if they don't complete checkout. Use this on email-gate or "reserve your order" steps to feed abandoned-cart recovery and lead capture flows. Carts can be referenced later or converted into an order via Create Order.
```javascript title="Create a Cart"
var payload = {
"user": {
"email": "test@email.com",
"first_name": "John",
"last_name": "Doe"
},
"lines": [
{
"package_id": 1
}
],
"attribution": {
"utm_source": "Example Campaign"
}
}
const response = await fetch('https://campaigns.apps.29next.com/api/v1/carts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
},
body: JSON.stringify(payload),
});
const result = await response.json()
console.log(result); // Show result in console
```
### Create Order [#create-order]
Creating an order is the core method in an external checkout flow, see the example below to familiarize yourself with the payload data required.
All orders require a `success_url` to handle payments requiring a redirect flow. The `success_url` should be the absolute URL of the "Next Page" in your campaign flow. In most cases, this should be your first upsell page, see more below in [Adding Upsells](#adding-upsells) on retrieving order details and handling payment methods that support upsells.
```javascript title="Create an Order"
var payload = {
"user": {
"email": "test@email.com",
"first_name": "John",
"last_name": "Doe"
},
"lines": [
{
"package_id": 1,
"properties": { // optional key/value pairs for customized products
"engraving": "Best Dad Ever",
"font": "Serif"
}
},
{
"package_id": 2,
"is_upsell": true
}
],
"shipping_address": {
"first_name": "string",
"last_name": "string",
"line1": "string",
"line4": "string",
"state": "string",
"postcode": "string",
"phone_number": "string",
"country": "US"
},
"billing_same_as_shipping_address": false,
"payment_detail": {
"payment_method": "card_token",
"card_token": "test_card" // See iFrame Payment Form Guide
},
"shipping_method": 1,
"success_url": "https://your-campaign.com/next-page/", // Next Page in Flow
"payment_failed_url": "https://your-campaign.com/decline-flow/", // Required without an HTTP Referer
"attribution": {
"utm_source": "Example Campaign"
}
}
const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
},
body: JSON.stringify(payload),
});
const result = await response.json();
console.log(result); // Show result in console
```
Always send `payment_failed_url` when creating an order. The API accepts an omitted value only when the request includes an HTTP `Referer` header. Some mobile and in-app browsers, referrer policies, and affiliate redirect chains omit that header, which causes the order request to fail when no explicit failure URL is present.
Bankcard payments require using the [iFrame Payment Form](/docs/admin-api/guides/payment-methods/bankcard) and passing the generated `card_token` for secure transfer of the payment method details. View a fully functional [Demo](https://nextcommerceco.github.io/demo-iframe-payment-form/).
* If response data has a `number`, order was successfully created, you can redirect to the next page.
* If response data has a `payment_complete_url`, redirect the user to this page. After payment, user will come back to your `success_url` or `payment_failed_url`.
If an APM redirect flow declines or is canceled, the user returns to `payment_failed_url`. When that field is omitted and the request includes an HTTP `Referer`, the referrer is used instead with contextual query strings, such as `?payment_failed=true&payment_method=paypal`.
Pass `payment_failed_url` to keep decline handling consistent when the `Referer` header is unavailable.
### Adding Upsells [#adding-upsells]
To add an upsell to an existing order, first you should check to see if the order payment method `supports_post_purchase_upsells` is `True` in the `orderRetrieve` response.
```javascript title="Retrieve Order Details"
const refId = ''
const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/' + refId + '/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
}
});
const result = await response.json();
console.log(result); // Show result in console
```
If the order `supports_post_purchase_upsells`, you can add an upsell to an order can be done using the `orderUpsellCreate` API endpoint.
```javascript title="Add Upsell to Order"
const refId = ''
var payload = {
"lines": [
{
"package_id": 1
}
]
}
const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/' + refId + '/upsells/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
},
body: JSON.stringify(payload),
});
const result = await response.json();
console.log(result);
```
### Order Confirmation [#order-confirmation]
On the order confirmation page, you can retrieve the order details and map the values to your template to show an order summary to the customer.
```javascript title="Retrieve Order Details"
const refId = ''
const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/' + refId + '/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
"Authorization": '' // Campaign API Key
}
});
const result = await response.json();
console.log(result); // Show result in console
```
# Getting Started with OAuth (/docs/apps/oauth/getting-started)
Server-side Apps that use Stores' Admin API must obtain authorization using OAuth 2.0 ([see overview](/docs/apps/oauth)). This guide shows you how to authorize your app and retrieve your Access Token to access the Admin API.
### Step 1: Retrieve API Credentials [#step-1-retrieve-api-credentials]
To get started, make sure that you have your Apps' `client_id` and `client_secret` available on the App details in your Partner account.
### Step 2: App Permissions Setup [#step-2-app-permissions-setup]
During the App installation flow, Apps that have `oauth` configured will be redirected to the Oauth App URL from thee App Settings.
Your app should redirect the user back to the store authorization view configured with the scope permissions your app requires.
At every stage in the Oauth flow you'll receive a querystring variable `store` with the network domain of the store that is installing the app. **You should use this in your app logic as the unique identifier for the store.**
```bash title="Authorization Link Format"
https://{network_domain}/oauth2/authorize/?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope={scopes}
```
| Parameter | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `network_domain` | The store network domain that is installing the app. Can be referenced from the `store` url parameter sent to your App Oauth App URL. |
| `response_type` | Must be `code`, which only authorization flow supported at this time. |
| `client_id` | Your app `client_id` found in in your partner account. |
| `redirect_uri` | The url you want to receive the Authorization Code in your app. **Must be listed in your app Redirect URLs setting**. |
| `scope` | A space separated list of scopes such as `orders:read orders:write users:read users:write`. [See list of all available scopes](/docs/admin-api/permissions). |
### Step 3: Confirm Installation [#step-3-confirm-installation]
After user click's Authorize to confirm App installation, it will redirect to the `redirect_uri` with `?store={network_domain}&code={authorize_code}` appended.
```bash title="Example"
https://yourapp.com/setup/authorize/?store={network_domain}&code={authorization_code}
```
| Parameter | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `network_domain` | The store network domain that is installing the app. Can be referenced from the `store` url parameter sent to your app Oauth Redirect URL. |
| `authorization_code` | The authorization code used to retrieve the Access Token in the next step. |
### Step 4: Retrieve Access Token [#step-4-retrieve-access-token]
After you have the `authorization_code`, you then need to retrieve the access token to gain access to the Admin API.
**Send a POST Request to `https://{network_domain}/oauth2/token/` to retrieve your access token**
Post request must be sent as `application/x-www-form-urlencoded` format.
```json title="Post Request to Retrieve Access Token"
{
"grant_type": "authorization_code",
"client_id": "{client_id}",
"client_secret": "{client_secret}",
"redirect_uri": "{redirect_uri}",
"code": "{authorize_code}"
}
```
A successful request will have the following response.
```json title="Response with Access Token"
{
"access_token": "{access_token}",
"expires_in": 15778476000,
"token_type": "Bearer",
"scope": "{scopes}",
"refresh_token": "{refresh_token}"
}
```
Save the `access_token` to your app to use with requests to the [Admin API](/docs/admin-api) for the store. :clap:
To see this in action, see the [Example App](https://github.com/NextCommerceCo/example-app) on Github.
# OAuth Overview (/docs/apps/oauth)
This guide introduces OAuth Authentication for Server-side Apps to access the Admin API.
### Introduction to OAuth [#introduction-to-oauth]
OAuth 2.0 is the industry standard protocol for authorizing and assigning permissions to 3rd party apps. There are many great guides on the internet regarding OAuth 2.0, such as this [OAuth 2.0 introduction guide from Auth0.com](https://auth0.com/intro-to-iam/what-is-oauth-2/). Your Server-side App's language most likely has pre-built packages to assist with handling Oauth 2.0 Authentication flows.
### Access Tokens [#access-tokens]
OAuth 2.0 uses **Access Tokens** which represent authorization to access resources on behalf of the end-user, ie access the Admin API. During the setup flow for your app, you'll be able to obtain request required permissions, get authorization from a user and retrieve a long lived access token to use for all future access to the Admin API.
### OAuth Setup Flow [#oauth-setup-flow]
Next Commerce uses OAuth 2.0's Authorization Code Flow to issue an access token on behalf of users.
##### Authorization Flow [#authorization-flow]
##### Authorization Flow Step Detail [#authorization-flow-step-detail]
1. User initiates the App installation process.
2. Store redirects to the App URL configured in App Oauth Settings.
3. App redirects to the store to load the OAuth Authorization view and requests the merchant to authorize app and permission required scopes, [see authorization link example](/docs/apps/oauth/getting-started#step-2-app-permissions-setup).
4. User authorizes the app and requested permission scopes in the store dashboard.
5. Store redirects to the Redirect URL with an [Authorization Code](https://oauth.net/2/grant-types/authorization-code/), a temporary credential representing the authorization, [see authorization code example](/docs/apps/oauth/getting-started#step-3-confirm-installation).
6. The app requests an Access Token using the Authorization Code, [see example access token request](/docs/apps/oauth/getting-started#step-4-retrieve-access-token).
7. Store returns an Access Token, [see example access token response](/docs/apps/oauth/getting-started#step-4-retrieve-access-token).
8. The app can now access the Admin API using the Access Token, [see Admin API examples](/docs/admin-api).
### Oauth Guides [#oauth-guides]
# Install Flows (/docs/apps/oauth/install-flows)
### Private Apps [#private-apps]
All apps start in a `private` state while the app developer is building and testing their app internally. While an app is `private`, you can use the "Install Link" builder form on your app detail page.
You may also want to trigger the install flow from your app's UI, to do this you can add a form to build the install link for your user to start the install flow on their store.
```bash title="Install Link"
https://{store_subdomain}.29next.store/dashboard/apps/install-app/?client_id={client_id}
```
| Parameter | Description |
| ----------------- | ------------------------------------------------ |
| `store_subdomain` | The store subdomain. |
| `client_id` | Your app Client ID found on the app detail page. |
### Public Apps [#public-apps]
Public apps show in all store dashboards and can be installed by any merchant at any time. If you plan to publish your app, ensure your app handles the install flow with a good user experience that guides the user through the process with your app.
To get your app published to our Public App store, see the [review and publishing guide](/docs/apps/review).
# Session Token Overview (/docs/apps/oauth/session-auth)
Session tokens are a method your app can use to authenticate users and requests from Next Commerce and your App.
### How Session Tokens Work [#how-session-tokens-work]
Session tokens follow the [JSON Web Tokens](https://jwt.io/introduction) standard. JWT tokens are signed objects your app can use to authenticate users to your app, see the example decoded token below. The JWT token is appended to requests to your app in the `token` parameter.
The JWT token can be verified with your App `CLIENT ID` and `CLIENT SECRET`, see example below.
```python title="JWT Token Decrypt & Verify"
import jwt
token = jwt.decode(
, ,
audience=
algorithms=["HS256"]
)
print(token)
```
```json title="JWT Token Decoded"
{
"iss": "https://example.29next.store/dashboard",
"sub": "https://example.29next.store",
"aud": "kbCWYEPXMezpXripKyDSlA68fb4auutiaEXmh3Rx",
"exp": 1660881585,
"nbf": 1660881525,
"iat": 1660881525,
"uid": 3,
"user_email": "demo@29next.com",
"store": "example.29next.store"
}
```
Tokens include details of the store and the store user (id & email) making the request. A valid token can be trusted as a verified request from a store with your app installed.
### Token Expiration [#token-expiration]
Tokens are shortlived with an expiration of 30 seconds, meaning they quickly expire and cannot be reused. It is recommended that you authenticate the users into your App with every request.
### Session Token Flow [#session-token-flow]
Session tokens are generated from the store dashboard and can be used by your App to verify request authenticity before authenticating the user to your app.
To see this in action, see the [Example App](https://github.com/NextCommerceCo/example-app) on Github.
# Dispute Service Apps (/docs/apps/guides/dispute-service)
Dispute service apps are integrations that manage the processing of payment disputes (alerts and chargebacks) on behalf of merchants seamlessly within the platform.
Dispute Service Apps are [Server to Server Apps](/docs/apps/guides/server-to-server-apps) that use the Oauth flow to obtain API Access and then use the [Admin APIs](/docs/admin-api) and [Webhooks](/docs/webhooks) to subscribe to store event activity.
## Disputes [#disputes]
Disputes are complaints initiated by the customer against the merchant pertaining to their orders and are categorized into two broad groups by type, Alerts and Chargebacks, see below.
* **Alerts** - disputes that are not yet a chargeback, ie TC40, SAFE, and RDR alerts.
* **Chargeback** - disputes that are registered as a chargeback with the acquiring bank.
## Dispute Flow Overview [#dispute-flow-overview]
Below is a high-level overview of a typical flow for a dispute service app to handle transaction disputes.
## Dispute Flow Detail [#dispute-flow-detail]
### Step 1 - New Transaction Created [#step-1---new-transaction-created]
There are several scenarios that create new payment transactions:
* Order created - nearly all new orders start with a new payment transaction.
* Upsell created - adding an upsell creates a new payment transaction.
* Refund created - refunding a order creates a refund transaction.
Orders typically have many associated transactions for the payments and refunds.
### Step 2 - `transaction.created` webhook event [#step-2---transactioncreated-webhook-event]
Dispute service apps should subscribe to the `transaction.created` webhook event to be notified of all payment transaction events on a store so that your app is aware of new payments, refunds, and can handle disputes properly.
Transaction `parent_id` will show the related transaction in the following cases:
* `refund` transactions - the parent is the debit transaction.
* `capture` transactions - the parent is the authorization transaction.
* `void` transactions - the parent is the authorization transaction.
Your app can also subscribe to `order.created` and `order.updated` events to receive more detailed order information such as items purchased, fulfillment tracking numbers, and items that have already been refunded.
Create your webhook after the app is installed using the [webhooksCreate](/docs/admin-api/reference/webhooks/webhooksCreate) Admin API and pass the `transaction.created, order.created, order.updated` to subscribe to these events.
### Step 3 - Dispute service receives transaction dispute [#step-3---dispute-service-receives-transaction-dispute]
At this stage, the dispute service is responsible for receiving disputes from their integration partners.
### Step 4 - Dispute service creates dispute (alert or chargeback) in store [#step-4---dispute-service-creates-dispute-alert-or-chargeback-in-store]
When disputes are identified as belonging to a store, the dispute service should create a new dispute using the [disputesCreate](/docs/admin-api/reference/payments/disputesCreate) Admin API.
### Step 5 - Dispute service matches dispute to transaction [#step-5---dispute-service-matches-dispute-to-transaction]
Disputes created in the store need to be matched to the transaction in the store to associate it with the order/customer. Disputes can be matched by passing the `transaction` parameter when creating or with update using the [disputesUpdate](/docs/admin-api/reference/payments/disputesUpdate) Admin API. See example below in [Match a Dispute](#matching-disputes).
Apple Pay, Google Pay, and other tokenized payment methods use virtualized card numbers that alert services like Ethoca may not match. See [Matching Tokenized Payments](#matching-tokenized-payments) for fallback strategies using `auth_code`.
### Step 6 - Customer is added to block lists [#step-6---customer-is-added-to-block-lists]
Merchants can configure their store to automatically add customers to block lists when any of their transactions have been disputed to mitigate future risk from the customer and associated payment methods. See [Block Lists](https://docs.nextcommerce.com/docs/features/payments/block-lists) guide in our user docs.
### Step 7 - Dispute service creates refund [#step-7---dispute-service-creates-refund]
Depending on the type of dispute, the dispute service may need to create a refund using the [transactionsRefundCreate](/docs/admin-api/reference/payments/transactionsRefundCreate) Admin API to resolve the dispute. See [Create a Refund](#creating-refunds) detail below.
For RDR alerts, the refund is already processed by the gateway. Your app should log it as an external refund with `is_external: true` to keep the store's transaction record accurate. See [RDR Alerts](#rdr-alerts) below.
### Step 8 - Cancel Order / Cancel Fulfillment [#step-8---cancel-order--cancel-fulfillment]
If the order is not yet fulfilled, it may be ideal to cancel the order or cancel fulfillment to stop the order from being shipped to the customer. See [Canceling Fulfillment](#canceling-fulfillment) detail below.
### Step 9 - Dispute service resolves dispute [#step-9---dispute-service-resolves-dispute]
Once the dispute is resolved, the dispute service should set the dispute resolution using the [disputesUpdate](/docs/admin-api/reference/payments/disputesUpdate) Admin API. See [Dispute Resolutions](#dispute-resolutions) and [Resolve a Dispute](#resolving-disputes) detail below.
## Creating Disputes [#creating-disputes]
To create a dispute in the store using the [disputesCreate](/docs/admin-api/reference/payments/disputesCreate) Admin API, see example below:
```json title="__http:POST:https://{store}.29next.store/api/admin/disputes/"
{
"type": "alert", // dispute type
"arn": "string", // optional
"case_number": "string", // optional
"happened_at": "2019-08-24T14:15:22Z", // date when dispute occurred
"amount": "string", // dispute amount, sometimes doesnt match transaction amount
"currency": "USD", // dispute currency
}
```
## Matching Disputes [#matching-disputes]
To match a dispute to a transaction, pass the `transaction` id to the [disputesUpdate](/docs/admin-api/reference/payments/disputesUpdate) API. Matching the dispute to the transaction will match the associated order and customer.
```json title="__http:PUT:https://{store}.29next.store/api/admin/disputes/{id}/"
{
"transaction": 7388
}
```
### Matching Tokenized Payments [#matching-tokenized-payments]
Apple Pay, Google Pay, and other tokenized payment methods present a **virtualized card number** (BIN and last 4 digits) to the payment network. Because the virtual card number differs from the cardholder's physical card, pre-chargeback alert services like Ethoca often cannot match the dispute to the transaction using the partial card number alone — these cases typically come back as "not found."
When a card-based lookup returns no match, use the `auth_code` field as a fallback to search for the originating transaction. The authorization code is issued by the card issuer and remains consistent regardless of whether the payment was tokenized, making it a reliable secondary identifier.
Use the [transactionsList](/docs/admin-api/reference/payments/transactionsList) API to search transactions by `auth_code`:
```json title="__http:GET:https://{store}.29next.store/api/admin/transactions/?auth_code={auth_code}"
```
The `auth_code` field is not supported by all payment gateways. Your app should implement a lookup strategy that falls back gracefully — for example, try card-based matching first and fall back to `auth_code`, or vice versa depending on the payment method.
The `auth_code` and `network_transaction_id` fields were added to the Transactions API and webhook events to assist with dispute mapping. See the [changelog](https://changelog.nextcommerce.com/blog/detail/18032026/) for details.
## Creating Refunds [#creating-refunds]
To create a refund for a transaction as part of the dispute resolution process, you can use the [transactionsRefundCreate](/docs/admin-api/reference/payments/transactionsRefundCreate) Admin API.
```json title="__http:POST:https://{store}.29next.store/api/admin/transactions/{id}/refund/"
{
"amount": "XX.XX", // refund amount
}
```
RDR alerts are automatically refunded by the gateway before your app is notified. To keep the store's transaction record in sync, create the refund with `is_external: true` so the platform logs the refund without attempting to process it again. See [RDR Alerts](#rdr-alerts) for the full pattern.
## Canceling Fulfillment [#canceling-fulfillment]
It's desirable to cancel fulfillment for orders that have not shipped yet when they are disputed to prevent additional losses for the merchant.
To retrieve a list of all fulfillment orders and their status, use the [ordersFulfillmentOrdersRetrieve](/docs/admin-api/reference/orders/ordersFulfillmentOrdersRetrieve) endpoint.
If order `fulfilmlent_status` is `unfulfilled`, your dispute service can stop fulfillment using the [fulfillmentOrdersHold](/docs/admin-api/reference/fulfillment/fulfillmentOrdersHold) endpoint.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/hold/"
{
"reason": "other",
"reason_message": "Order disputed by customer." // Use a relevant other reason message
}
```
If order `fulfilmlent_status` is `processing` your dispute service can request processing fulfillment orders be canceled with the fulfillment locations with the [cancellationRequestSend](/docs/admin-api/reference/fulfillment/cancellationRequestSend) endpoint.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/cancellation-request/"
{
"message": "Order disputed by customer" // Fulfillment cancel reason
}
```
Fulfillment locations need to accept the cancelation request to confirm they were able to stop fulfillment on their end at. It is possible that the fulfillment order was already shipped and the fulfillment could not be stopped.
## RDR Alerts [#rdr-alerts]
RDR Alerts are automatically refunded with the gateway, dispute services should log an external refund for the transaction using the [transactionsRefundCreate](/docs/admin-api/reference/payments/transactionsRefundCreate) Admin API.
Setting `is_external: true` on a refund will create the refund without attempting the refund with the gateway.
```json title="__http:POST:https://{store}.29next.store/api/admin/transactions/{id}/refund/"
{
"amount": "XX.XX", // refund amount
"is_external": true // set for external refunds
}
```
## Dispute Resolutions [#dispute-resolutions]
**Alert Resolutions**
* `could_not_find_order` - You could not match the alert to an order or transaction.
* `declined_or_canceled_nothing_to_do` - Customer had declined or canceled the order; no further action is necessary.
* `issued_full_refund` - Issued a full refund after the dispute was created.
* `issued_refund_for_remaining_amount` - Order was already partially refunded so you issued a refund for the remaining amount.
* `3ds_authorized_successfully` - Order was approved by 3D Secure so the customer's bank is responsible for the dispute.
* `previously_refunded_nothing_to_do` - Order was already refunded before the alert was issued; no further action is necessary.
* `unable_to_refund_merchant_account_closed` - You are unable to refund the order because your merchant account has been closed.
* `other` - None of the available resolutions matched the outcome of this alert.
**Chargeback Resolutions**
* `won` - The chargeback representment process was successful and was resolved in favor of the merchant.
* `lost` - The chargeback representment process was unsuccessful.
## Resolving Disputes [#resolving-disputes]
To resolve a dispute, update the dispute with the appropriate [resolution](#dispute-resolutions) for the dispute type.
```json title="__http:POST:https://{store}.29next.store/api/admin/disputes/{id}/"
{
"resolution": "issued_full_refund"
}
```
# Fulfillment Service Apps (/docs/apps/guides/fulfillment-service)
Fulfillment service apps are integrations that manage fulfillment of physical products for merchants by enabling transparent communication between fulfillment providers and merchants using the Next Commerce dashboard.
View a fully functional [Demo Fulfillment Service App](https://github.com/NextCommerceCo/demo-fulfillment-service-app) to see all of the concepts in action with detailed code examples.
## Fulfillment Flow Overview [#fulfillment-flow-overview]
Below is a high-level overview of the fulfillment flow for fulfillment services to accept and process assigned fulfillment orders.
### Fulfillment Flow Detail [#fulfillment-flow-detail]
### Step 1 - New Order Created [#step-1---new-order-created]
All orders start with a customer creating a new order in the Checkout Flow, Admin API, or a recurring Subscription. Orders contain multiple [Fulfillment Orders](#fulfillment-orders), each with the products allocated to the same [Fulfillment Location](#fulfillment-locations) that has the products in stock.
### Step 2 - Store Creates a Fulfillment Request [#step-2---store-creates-a-fulfillment-request]
After a delay period (usually several hours), the store will send a fulfillment request to the [Fulfillment Location](#fulfillment-locations) assigned to the order. Fulfillment Requests can be initiated by dashboard users, background processes, or via the Admin API. See [Fulfillment Locations](#fulfillment-locations).
### Step 3 - Fulfillment Service Retrieves Assigned Fulfillment Orders [#step-3---fulfillment-service-retrieves-assigned-fulfillment-orders]
In response to receiving a fulfillment request, the fulfillment service needs to retrieve all of their assigned fulfillment orders from the [Assigned Fulfillment Orders](/docs/admin-api/reference/fulfillment/assignedFulfillmentOrdersList) API endpoint. See [Assigned Fulfillment Orders](#assigned-fulfillment-orders).
### Step 4 - Fulfillment Service Accepts Fulfillment Request [#step-4---fulfillment-service-accepts-fulfillment-request]
For each assigned fulfillment order request in Step 3, the fulfillment service needs to `Accept` or `Reject` the assignment to notify the store the order is expected to be fulfilled or not. See [Accepting Fulfillment Requests](#accepting-fulfillment-requests).
### Step 5 - Fulfillment Service Processes Fulfillment Order with Carrier [#step-5---fulfillment-service-processes-fulfillment-order-with-carrier]
Accepted fulfillment orders are processed by the fulfillment service location to prepare shipment to the customer.
### Step 6 - Fulfillment Service Creates a Fulfillment [#step-6---fulfillment-service-creates-a-fulfillment]
Once an order has been fulfilled, the fulfillment service creates a fulfillment to upload with shipment carrier tracking information and notify the customer their order has shipped. See [Creating Fulfillments](#creating-fulfillments).
### Step 7 - Carrier Delivers Ordered Products to Customer [#step-7---carrier-delivers-ordered-products-to-customer]
The carrier is responsible for delivering the products to the customer in this stage.
## Fulfillment Orders [#fulfillment-orders]
A Fulfillment Order represents items in an order that are to be fulfilled from the same location. A single order often contains multiple fulfillment orders, it's important to keep this in mind for your integration.
## Fulfillment Locations [#fulfillment-locations]
Fulfillment Services need to create `Locations` which represent their warehouses where physical products are stored and fulfilled from. Product `stockrecords` must be associated with a location for fulfillment.
When new orders are created, the fulfillment orders are assigned to the locations based on [Fulfillment Routing](https://docs.nextcommerce.com/docs/features/fulfillment-guide/location-based-routing). The Location Address is also used for Tax calculation. See the [Locations Create](/docs/admin-api/reference/fulfillment/locationsCreate) endpoint to create a location.
### Location Callback [#location-callback]
The location `callback` is a URL the store will send `Fulfillment Request` webhooks to notify them of a new fulfillment assigned. Fulfillment Services need to query their [Assigned Fulfillment Orders](#assigned-fulfillment-orders) to retrieve the fulfillment order details.
Fulfullment Requests are sent to the location `callback` + `/fulfillment-order-notification/`.
**Fulfillment assignment request types are:**
* `fulfillment_requested` - when a new fulfillment order has been requested to be fulfilled.
* `cancellation_requested`- when an already processing fulfillment order has been requested to cancel fulfillment.
```json title="Example Fulfillment Request Payload"
{
"type": "fulfillment_requested"
}
```
```json title="Example Cancellation Request Payload"
{
"type": "cancellation_requested"
}
```
The webhook request has a `X-29Next-Store` header that indicates which store the request is from if you have a global callback url.
## Assigned Fulfillment Orders [#assigned-fulfillment-orders]
The [Assigned Fulfillment Orders](/docs/admin-api/reference/fulfillment/assignedFulfillmentOrdersList) will return a list of all fulfillment orders assigned to your locations. Your app is recommended to call this API in response to receiving a notification to the location callback, and you may also want to poll this endpoint occasionally (ie once per hour) to ensure you've actioned everything requested.
Use the appropriate `assignment_status` parameter to filter the fulfillment orders to those that require action.
Retrieve all pending Fulfillment Requests
```json title="__http:GET:https://{store}.29next.store/api/admin/assigned-fulfillment-orders/?assignment_status=fulfillment_requested"
{}
```
Retrieve all pending Cancellation Requests
```json title="__http:GET:https://{store}.29next.store/api/admin/assigned-fulfillment-orders/?assignment_status=cancellation_requested"
{}
```
Requests to the location callback indicate the request `type` that should be passed to the [Assigned Fulfillment Orders](/docs/admin-api/reference/fulfillment/assignedFulfillmentOrdersList) API as the `assignment_status` querystring value to filter fulfillment orders to those for the flow.
For example, to see new fulfillment requests pending acceptance, pass `assignment_status=fulfillment_requested` to the Assigned Fulfillment Orders API, `fulfillment_requested` was passed as `type` to the callback.
## Customized Products [#customized-products]
Customized or personalized products carry their customization details in the line item `properties`. When an order is created with line item `properties` (see [Custom Line Item Properties](/docs/admin-api/guides/external-checkout#custom-line-item-properties)), those values flow through to the fulfillment order line items so your fulfillment service can fulfill the customization.
Each `line_item` returned from the [Assigned Fulfillment Orders](#assigned-fulfillment-orders) API includes a `properties` object with the customization details to fulfill.
```json title="Fulfillment Order Line Item Properties"
"line_items": [
{
"product_title": "Engraved Bottle",
"sku": "BOTTLE-001",
"quantity": 1,
"properties": {
"engraving": "Best Dad Ever",
"font": "Serif",
"gift_message": "Happy Birthday!"
}
}
]
```
Line item `properties` also determine the uniqueness of line item products — identical products with different `properties` are kept as separate line items.
## Accepting Fulfillment Requests [#accepting-fulfillment-requests]
To `Accept` a Fulfillment Request assignment, send a POST request to the [Fulfillment Request Accept](/docs/admin-api/reference/fulfillment/fulfillmentRequestAccept) Endpoint. The Fulfillment Order will show as **Accepted** in the dashboard and the Order will transition fulfillment\_status to processing.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillment-request/accept/"
{}
```
## Rejecting Fulfillment Requests [#rejecting-fulfillment-requests]
Your fulfillment service can `Reject` fulfillment order assignments which will prompt the merchant to take action in their store to amend the order. Rejecting fulfillment requests can be for many reasons, such as incorrectly assigned for unavailable products, no stock available, or invalid address. The Fulfillment Service should verify the order details before accepting, by rejecting the fulillment request it will prompt the merchant to correct the order and then send an updated fulfillment request. See possible rejection reasons on in the [Fulfillment Request Reject API docs](/docs/admin-api/reference/fulfillment/fulfillmentRequestReject).
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillment-request/reject/"
{
"rejected_reason": "incorrect_address",
"message": "Your Message"
}
```
## Cancellation Flow Overview [#cancellation-flow-overview]
It is common for customers to contact merchants after orders have already been sent to fulfillment service partners for processing. Merchants have the ability to "Request Fulfillment Cancellation" which will send a `cancellation_request` to your [Location Callback](#location-callback). Fulfillment Services are expected to respond to these requests.
### Cancellation Flow Detail [#cancellation-flow-detail]
### Step 1 - Store Creates Cancellation Request [#step-1---store-creates-cancellation-request]
Merchants will trigger a cancellation request which will send a cancellation request to the [Fulfillment Location](#fulfillment-locations) assigned to the order.
### Step 2 - Fulfillment Service Retrieves Cancellation Requests [#step-2---fulfillment-service-retrieves-cancellation-requests]
In response to receiving a cancellation request, the fulfillment service needs to retrieve all of its assigned cancellation requests from the [Assigned Fulfillment Orders](/docs/admin-api/reference/fulfillment/assignedFulfillmentOrdersList) API endpoint.
```json title="__http:GET:https://{store}.29next.store/api/admin/assigned-fulfillment-orders/?assignment_status=cancellation_requested"
{}
```
### Step 3 - Fulfillment Service Accepts/Rejects Cancellation Request [#step-3---fulfillment-service-acceptsrejects-cancellation-request]
At this stage, the Fulfillment Service needs to [Accept](#accepting-cancellation-requests) or [Reject](#rejecting-cancellation-requests) the cancellation request to respond to the merchant's request.
## Accepting Cancellation Requests [#accepting-cancellation-requests]
To `accept` the cancellation request, send a request to the [Cancellation Request Accept API](/docs/admin-api/reference/fulfillment/cancellationRequestAccept).
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/cancellation-request/accept/"
{}
```
## Rejecting Cancellation Requests [#rejecting-cancellation-requests]
If an fulfillment order is too far along in fulfillment processing, fulfillment service partners can reject cancellation requests.
To `reject` a cancellation request, send a request to the [Cancellation Request Reject API](/docs/admin-api/reference/fulfillment/cancellationRequestReject). If you include a message, it will show in the order timeline events for the merchant to see.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/cancellation-request/reject/"
{
"message": "Order already shipped."
}
```
## Creating Fulfillments [#creating-fulfillments]
Once you have tracking information for your the outgoing shipment to the customer, create a Fulfillment for the fulfillment order on the [Fulfillments Create](/docs/admin-api/reference/fulfillment/fulfillmentsCreate) API.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillments/"
{
"notify": true, // send the customer an order shipped notification email
"tracking_info": [
{
"tracking_code": "EXAMPLECODE",
"carrier": "dhl_ecommerce"
}
]
}
```
## Partial Fulfillments [#partial-fulfillments]
Sometimes a fulfillment order can't be shipped in a single shipment — for example when items are backordered, or part of the order is ready early. The [Fulfillments Create](/docs/admin-api/reference/fulfillment/fulfillmentsCreate) endpoint supports partial fulfillments by accepting the optional `fulfillment_order_line_items` array. Pass only the line items (and quantities) you are ready to ship in this call, and the remaining items stay on the original fulfillment order for a later fulfillment.
### Behavior [#behavior]
* The fulfilled line items are moved from the current fulfillment order into a new closed fulfillment order.
* The remaining items stay on the original fulfillment order, which stays open waiting to be fulfilled.
### Example Partial Fulfillment Request [#example-partial-fulfillment-request]
The example below fulfills 1 of 2 units of `line_item` `101` and leaves the other unit — along with all of `line_item` `102` — on the original fulfillment order.
```json title="__http:POST:https://{store}.29next.store/api/admin/fulfillment-orders/{id}/fulfillments/"
{
"notify": true,
"fulfillment_order_line_items": [
{
"id": 101,
"quantity": 1
}
],
"tracking_info": [
{
"tracking_code": "EXAMPLECODE",
"carrier": "dhl_ecommerce"
}
]
}
```
Omit `fulfillment_order_line_items` (as in the standard [Creating Fulfillments](#creating-fulfillments) example) to fulfill every remaining item on the fulfillment order in one call.
## Sync Product Inventory [#sync-product-inventory]
Products have "stock records" which represent the available physical stock at a fulfillment location. Often a single product can be stocked at multiple locations, with [Fulfillment Routing](https://docs.nextcommerce.com/docs/features/fulfillment-guide/location-based-routing) governing the order fulfillment location assignment.
### Retrieve Stock Records by Location [#retrieve-stock-records-by-location]
Fulfillment services can retrieve all stock records from a store to map with the SKUs at their warehouse. You can also search and filter by product name or SKU, see Admin API docs for [stockrecordsRetrieve](/docs/admin-api/reference/products/stockrecordsRetrieve).
```json title="__http:GET:https://{store}.29next.store/api/admin/stockrecords/?location_id={id}"
{}
```
With the list of stock records assigned to your location, you can now update the `num_in_stock` to reflect the current available units.
### Update Number of Units In Stock [#update-number-of-units-in-stock]
Fulfillment services are recommended to update the number in stock (`num_in_stock`) at regular intervals so that the store inventory is update to date and accurate.
```json title="__http:PATCH:https://{store}.29next.store/api/admin/stockrecords/{id}/"
{
"num_in_stock": 1000
}
```
## Moving Fulfillment Orders [#moving-fulfillment-orders]
# Marketing Attribution Apps (/docs/apps/guides/marketing-attribution)
Marketing attribution apps connect conversion events with sales channels across storefront, campaigns, and API.
1. **Storefront Order Attribution** — Use [Event Tracking](/docs/apps/event-tracking) and the [GraphQL Storefront API](/docs/storefront/graphql) to capture platform-specific identifiers from the visitor's browser and store them as attribution metadata on the cart.
2. **Campaigns & API Order Attribution** — Subscribe to [Webhooks](/docs/webhooks) to receive order events server-side and extract the stored attribution metadata for conversion tracking.
## Attribution Flow Overview [#attribution-flow-overview]
## Storefront Order Attribution [#storefront-order-attribution]
Storefront order attribution runs in the visitor's browser via an [Event Tracker](/docs/apps/event-tracking) installed through your app's manifest. The event tracker needs to do three things:
1. **Capture platform identifiers** from browser cookies and URL parameters.
2. **Store identifiers as cart attribution metadata** using the [GraphQL Storefront API](/docs/storefront/graphql) so they persist through to the completed order.
3. **Fire client-side pixel events** by subscribing to [Storefront Events](/docs/storefront/event-tracking) for real-time browser-side tracking.
### App Setup [#app-setup]
If you haven't already, create your app on [Next Commerce Accounts](https://accounts.29next.com) and install [App Kit](/docs/apps/guides/storefront-extension#app-kit) to build and push your app files.
Your app's [manifest](/docs/apps/manifest) defines the storefront integration. Map a javascript file as the `storefront_event_tracker`, use `settings_schema` to allow merchants to configure platform-specific IDs, and add an [App Snippet](/docs/apps/snippets) to inject the ad platform's pixel script into the storefront.
```json title="manifest.json"
{
"storefront_event_tracker": "tracking.js",
"settings_schema": [
{
"name": "pixel_id",
"type": "text",
"label": "Pixel ID",
"default": "",
"required": 1,
"help_text": "Your ad platform pixel identifier.",
"max_length": 250
}
],
"locations": {
"storefront": {
"global_header": "snippets/global_header.html"
}
}
}
```
Apps can programmatically manage the merchant's settings values (e.g. Pixel ID) through the [appsSettingsUpdate](/docs/admin-api/reference/apps/appsSettingsUpdate) Admin API endpoint, allowing you to populate configuration from your own app dashboard.
The `global_header` snippet is where you load the ad platform's pixel script so that it sets the necessary browser cookies. The event tracker then reads these cookies and stores them on the cart.
```html title="snippets/global_header.html"
{% if app.settings.pixel_id %}
{% endif %}
```
### Event Tracker [#event-tracker]
When your app is installed, the event tracker is automatically created with the content of `tracking.js`. The merchant's settings are accessible via `app.settings` and the [GraphQL Storefront API](/docs/storefront/graphql) is available at `/api/graphql/`.
Below is an example event tracker that captures platform identifiers, stores them as cart attribution metadata via the `createCart` and `updateCartAttribution` GraphQL mutations, and subscribes to storefront events for client-side pixel tracking.
Ad platform pixel scripts set cookies asynchronously after page load. Your event tracker should wait for the relevant cookies to be set before attempting to read them. A timeout of \~5 seconds is typically sufficient.
```javascript title="tracking.js"
if (app.settings.pixel_id) {
// --- Helpers ---
function getCookie(name) {
return document.cookie
.split(";")
.find(c => c.trim().startsWith(`${name}=`))
?.split("=")[1];
}
function waitForCookie(name, timeout = 5000, interval = 500) {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = setInterval(() => {
if (getCookie(name)) {
clearInterval(check);
resolve(getCookie(name));
} else if (Date.now() - start >= timeout) {
clearInterval(check);
reject(new Error(`Timeout waiting for cookie: ${name}`));
}
}, interval);
});
}
function queryGraphql(query, variables = {}) {
return fetch("/api/graphql/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables })
}).then(r => r.json());
}
// --- GraphQL Mutations ---
const CREATE_CART = `mutation createCart {
createCart(input: {}) {
success
errors
cart { id, pk, attribution { metadata } }
}
}`;
const UPDATE_CART_ATTRIBUTION = `mutation updateCartAttrMetadata(
$cartId: String, $metadata: GenericScalar
) {
updateCartAttribution(
input: {cartId: $cartId, attribution: {metadata: $metadata}}
) {
success
errors
cart { id, pk, attribution { metadata } }
}
}`;
// --- Attribution Capture ---
async function captureAttribution() {
// 1. Create a cart
const cartData = await queryGraphql(CREATE_CART);
const cart = cartData.data?.createCart?.cart;
if (!cart) return;
// 2. Wait for ad platform cookies to be set
try {
await waitForCookie("your_cookie_name");
} catch (e) {
console.error(e);
}
// 3. Capture identifiers and store as cart attribution metadata
const metadata = cart.attribution?.metadata || {};
const browserId = getCookie("your_cookie_name");
const clickId = getCookie("your_click_cookie_name");
if (browserId) metadata.your_browser_id = browserId;
if (clickId) metadata.your_click_id = clickId;
metadata.pixel_id = app.settings.pixel_id;
await queryGraphql(UPDATE_CART_ATTRIBUTION, {
cartId: cart.id,
metadata: metadata
});
}
captureAttribution();
// --- Client-Side Pixel Events ---
analytics.subscribe("page_viewed", event => {
// Fire page view event on your ad platform
});
analytics.subscribe("product_viewed", event => {
// Fire view content event with event.data product details
});
analytics.subscribe("product_added_to_cart", event => {
// Fire add to cart event with event.data line item details
});
analytics.subscribe("checkout_started", event => {
// Fire initiate checkout event with event.data cart details
});
analytics.subscribe("checkout_completed", event => {
// Fire purchase/conversion event with event.data order details
});
}
```
See [Storefront Event Tracking](/docs/storefront/event-tracking) for the full list of available events and their data structures.
Attribution metadata stored on the cart is automatically carried over to the order when the customer completes checkout. This is what makes the webhook-based conversion flow possible — the identifiers captured in the browser are available in the `order.created` webhook payload.
## Campaigns & API Order Attribution [#campaigns--api-order-attribution]
Not all orders originate from the storefront. Orders created through [Campaigns](/docs/campaigns), the [Admin API](/docs/admin-api), and recurring [Subscription](/docs/admin-api/guides/subscription-management) renewals bypass the storefront entirely — [Webhooks](/docs/webhooks) are the way to capture attribution data for these conversions.
### Subscribe to Webhooks [#subscribe-to-webhooks]
Subscribe to the `order.created` webhook event using the [webhooksCreate](/docs/admin-api/reference/webhooks/webhooksCreate) Admin API endpoint. Your app should do this during the OAuth installation flow. Always [verify the webhook signature](/docs/webhooks#verifying-webhook-requests) before processing incoming events.
```json title="__http:POST:https://{store}.29next.store/api/admin/webhooks/"
{
"target": "https://your-app.example.com/webhooks/",
"events": ["order.created"],
"signing_secret": "your-signing-secret"
}
```
### Extract Attribution Metadata from Order [#extract-attribution-metadata-from-order]
The `order.created` webhook payload contains the full order data, including the `attribution` object with the metadata your event tracker stored on the cart.
```json title="order.created Webhook Payload (abbreviated)"
{
"event_type": "order.created",
"object": "order",
"data": {
...
"number": "109659",
"source": "storefront",
"total_incl_tax": "84.98",
"total_excl_tax": "84.98",
"currency": "USD",
"reporting_values": {
"currency": "USD",
"total_incl_tax": "84.98",
"total_excl_tax": "84.98",
"shipping_incl_tax": "4.99",
"shipping_excl_tax": "4.99",
"total_discount": "0.00"
},
"attribution": {
...
"metadata": {
"your_platform_id": "your_value",
"your_click_id": "your_value",
"your_browser_id": "your_value"
}
},
...
}
}
```
View the full [order.created webhook payload](/docs/webhooks/reference/orders/order.created) for all available fields including user, lines, shipping address, and transaction data.
# Server to Server Apps (/docs/apps/guides/server-to-server-apps)
Server-to-server apps leverage the [OAuth flow](/docs/apps/oauth) to obtain API access and then use the [Admin APIs](/docs/admin-api) and [Webhooks](/docs/webhooks) to subscribe to store event activity.
Server-to-server apps don't need to upload any code or files to Next Commerce; they are fully external and simply use OAuth to obtain Admin API access.
In this guide, we'll walk through creating your first app so you can get up and running with the core concepts.
## Preparation [#preparation]
1. If you haven't done so already, create your [Next Commerce account](https://accounts.29next.com) and complete your [Partner Registration](https://accounts.29next.com/partners/).
2. Make sure you have access to a store you plan to use for development - you will use this in later steps.
## Create Your App [#create-your-app]
1. Inside your Partner Account, navigate to Apps and then Create App. Follow the form to create your first app, don't worry about the App name and details, you can change this later.
2. After creating your App, you'll be able to see your App details. Take note of your `Client ID` and `Client Secret` which are used in the [OAuth flow](/docs/apps/oauth) to retrieve an API Access Token during the [App Install flow](/docs/apps/oauth/getting-started).
## Configure App OAuth [#configure-app-oauth]
Server-to-server apps use the [OAuth flow](/docs/apps/oauth) to get an Admin API access token. Let's add the [OAuth 2.0 ](https://oauthdebugger.com/) URLs to our app to simulate the install flow.
1. In **App > Settings**, add `https://oauthdebugger.com/debug` as your App URL and Redirect URL to configure your app to use OAuth Debugger.
2. Install your app on your store using the **Install Link** tool on your App Overview.
## Test App On Development Store [#test-app-on-development-store]
1. You can now use [OAuth Debugger](https://oauthdebugger.com/) to simulate the OAuth flow.
Your app should now be installed and ready for testing and further development. :raised\_hands:
# Storefront Extension Apps (/docs/apps/guides/storefront-extension)
In this guide we'll go over all of the steps to get started building an app that extends storefront functionality to introduce many of the core app framework concepts and how to use them.
## Common Use Cases [#common-use-cases]
Storefront extensions are commonly used for but not limited to:
* Install javascript tracking snippets to storefront and checkout flow, see [event tracking](/docs/apps/event-tracking).
## Preparation [#preparation]
1. If you haven't done so already, create your [Next Commerce account](https://accounts.29next.com) and complete your [Partner Registration](https://accounts.29next.com/partners/).
2. Make sure you have access to a store you plan to use for development - you will use this in later steps.
## App Layout Overview [#app-layout-overview]
Let's take a look at the basic file structure of apps to understand how to get started creating our first app.
```bash title="Example App Structure"
your-app
├── assets
│ └── my-app.js
└── manifest.json
```
## Manifest.json [#manifestjson]
A manifest.json file specifies the configuration and metadata needed to install and configure your app. [See Manifest Reference](/docs/apps/manifest)
## App Kit [#app-kit]
App Kit is a tool you install on your local machine to assist with bundling your app files and pushing them to the platform so they can be installed on a store.
## Setup App [#setup-app]
Now that you have your app files, it's time to configure your app on your local to link it to your App `Client ID`.
```bash title="Setup App Kit Authentication"
App Client ID: // your app client id
accounts.29next.com Email: your user account email
accounts.29next.com Password: your user account password
```
## Build Your App Bundle [#build-your-app-bundle]
Using App Kit, create a build of your app using the command line from your app directory.
```bash title="Build App"
nak build
```
## Push App [#push-app]
Now, push your app to your our platform using your username and password credentials for your account at `accounts.29next.com`.
```bash title="Push App"
nak push
```
# Storefront GraphQL API (/docs/storefront/graphql)
Build custom sidecarts, upsell flows, and dynamic storefront experiences with the Storefront GraphQL API. Query products, manage carts, apply vouchers, and handle user accounts — all from your theme's JavaScript or any client-side application.
## API Endpoint [#api-endpoint]
```bash title="Storefront GraphQL Endpoint"
https://{store}.29next.store/api/graphql/
```
All requests must be `POST` with `Content-Type: application/json`. Replace `{store}` with your store's subdomain.
## Authentication [#authentication]
The Storefront API is available within the context of your storefront on a storefront domain. Requests made from your theme's JavaScript automatically inherit the user's session — no API keys or tokens needed.
External access to the Storefront API (outside of the storefront context) will be available in future iterations.
## Interactive Explorer (GraphiQL) [#interactive-explorer-graphiql]
Every store includes a built-in **GraphiQL IDE** — an in-browser tool for writing, validating, and testing GraphQL queries directly against your store's schema. Navigate to `/api/graphql/` on your store to open it.
GraphiQL lets you:
* **Explore the schema** — browse all available queries, mutations, types, and their fields using the documentation sidebar
* **Build queries visually** — use the explorer panel to construct queries by selecting fields, without writing GraphQL by hand
* **Test in real-time** — run queries and mutations against your store and see the JSON response instantly
* **Validate syntax** — get inline error highlighting and autocomplete as you type
```bash title="Storefront GraphQL Endpoint"
https://{store}.29next.store/api/graphql/
```
This is the fastest way to learn what the API offers and prototype queries before adding them to your theme code.
## API Reference [#api-reference]
### Queries [#queries]
* [`cart`](/docs/storefront/graphql/queries/cart) — Retrieve a cart by ID
* [`me`](/docs/storefront/graphql/queries/me) — Get the current authenticated user
* [`product`](/docs/storefront/graphql/queries/product) — Fetch a single product
* [`products`](/docs/storefront/graphql/queries/products) — Query the product catalog
### Mutations [#mutations]
* [`createCart`](/docs/storefront/graphql/mutations/create-cart) — Create a new cart
* [`addCartLines`](/docs/storefront/graphql/mutations/add-cart-lines) — Add line items to a cart
* [`updateCartLines`](/docs/storefront/graphql/mutations/update-cart-lines) — Update quantities on existing cart lines
* [`removeCartLines`](/docs/storefront/graphql/mutations/remove-cart-lines) — Remove line items from a cart
* [`emptyCart`](/docs/storefront/graphql/mutations/empty-cart) — Remove all items from a cart
* [`addVoucher`](/docs/storefront/graphql/mutations/add-voucher) — Apply a voucher code to a cart
* [`removeVoucher`](/docs/storefront/graphql/mutations/remove-voucher) — Remove a voucher from a cart
* [`updateCartAttribution`](/docs/storefront/graphql/mutations/update-cart-attribution) — Set UTM and affiliate attribution on a cart
* [`updateCartMetadata`](/docs/storefront/graphql/mutations/update-cart-metadata) — Update custom metadata on a cart
* [`register`](/docs/storefront/graphql/mutations/register) — Register a new customer account
* [`tokenAuth`](/docs/storefront/graphql/mutations/token-auth) — Authenticate and obtain a token
* [`verifyToken`](/docs/storefront/graphql/mutations/verify-token) — Verify an authentication token
* [`updateAccount`](/docs/storefront/graphql/mutations/update-account) — Update the current user's account details
## Quick start [#quick-start]
Fetch the current cart to build a custom sidecart UI:
```bash title="Example: Query the cart"
curl -X POST https://yourstore.29next.store/api/graphql/ \
-H "Content-Type: application/json" \
-d '{
"query": "query Cart($id: ID!) { cart(id: $id) { id numItems totalInclTax currency lines { edges { node { id quantity product { title } linePriceInclTax } } } } }",
"variables": { "id": "" }
}'
```
```json title="Response"
{
"data": {
"cart": {
"id": "Q2FydE5vZGU6MTIz",
"numItems": 2,
"totalInclTax": "59.98",
"currency": "USD",
"lines": {
"edges": [
{
"node": {
"id": "Q2FydExpbmVOb2RlOjE=",
"quantity": 1,
"product": { "title": "Premium Supplement" },
"linePriceInclTax": "29.99"
}
}
]
}
}
}
}
```
## Common use cases [#common-use-cases]
### Custom sidecarts [#custom-sidecarts]
The most common use of the Storefront API is building custom sidecart experiences. Use `cart` queries to fetch the current cart state and render a fully custom cart drawer with your own markup, animations, and styling.
### Upsells and cross-sells [#upsells-and-cross-sells]
Add upsell products to the cart dynamically using mutations like `addCartLines`. Query the product catalog with `products` to find related items, then present them in your sidecart or on product pages.
```graphql title="Add an upsell to the cart"
mutation AddUpsell($input: AddCartLinesInput!) {
addCartLines(input: $input) {
success
cart {
numItems
totalInclTax
}
}
}
```
### User accounts [#user-accounts]
Handle customer registration and authentication directly from your storefront with `register`, `tokenAuth`, and `updateAccount` mutations.
### Vouchers and discounts [#vouchers-and-discounts]
Apply and remove voucher codes from carts using `addVoucher` and `removeVoucher` mutations, enabling custom promo code UIs.
# Storefront CDN & Caching (/docs/storefront/themes/cdn-and-caching)
Storefront leverages CDNs and many caching strategies to ensure fast performant user experiences for your end customers.
Always use the store network domain `https://{store}.29next.store` when developing, previewing, debugging, and verifying themes. Do not use a mapped public storefront domain to decide whether a theme change landed.
### Asset CDN [#asset-cdn]
All merchant uploaded media assets and theme assets are loaded from our CDN for the fastest performance.
* **Media** - Links to uploaded media should always use the `cdn.29next.store`
* **Theme Assets** - Theme assets should use the [asset\_url](/docs/storefront/themes/templates/filters#asset_url) in templates which always generates a full CDN link on the storefront.
### Full Page Caching [#full-page-caching]
All pages on storefront are cached for 5 minutes to ensure popular pages are as fast as possible for customers and minimal impact on the overall platform load.
* User is Anonymous (unauthenticated).
* Domain is a merchant mapped domain.
* Page is not dynamic, ie `/cart/`, `/checkout/`, `/accounts/` do not use full page caching.
Confirm the latest theme on `https://{store}.29next.store` first. After it is correct there, check the mapped public storefront domain as a final customer-path smoke test.
### Template Caching [#template-caching]
Themes use many templates ie `layouts`, `partials`, and `assets` that when compiled together create amazing customer experiences. Templates are cached in memory to reduce database queries when compiling templates into the full html response.
Updating a template through the dashboard or [Theme Kit](/docs/storefront/themes/theme-kit) should automatically purge the cache for you to see your latest changes on the network domain, see notes above.
There are a few cases wherein a form on the frontend needs to use a `{% csrf_token %}` field to secure submission to the backend. The platform core JS will automatically replace `{% csrf_token %}` that are in cached versions of pages to ensure the forms still work.
**It is advisable to not implement custom templates that require `{% csrf_token %}`, we recommend the [Storefront GraphQL API](/docs/storefront/graphql) instead.**
# Themes (/docs/storefront/themes)
We highly recommend using Theme Kit to manage your store theme for the best developer experience from your favorite IDE. [Read the Theme Kit guide](/docs/storefront/themes/theme-kit) — installation, configuration, and every `ntk` command.
### Layout & Structure [#layout--structure]
The Storefront theme framework has a set guideline for the base directories of your theme for your assets, html, and settings.
```bash title="Storefront Theme Structure"
theme
├── assets
├── checkout
├── configs
├── layouts
├── locales
├── partials
├── sass
└── templates
```
### Assets [#assets]
The assets directory is used to upload static asset files used in the theme such as images, stylesheets, web fonts, and javascript files. The assets directory works in conjunction with the [`asset_url` template filter](/docs/storefront/themes/templates/filters#asset_url) to render the full path on your storefront.
```jinja title="Example link in template to /assets/css/style.css"
{{ 'css/style.css'|asset_url }}
```
### Configs [#configs]
The configs directory is used to store your theme settings options and also the settings data as they should be configured with the theme.
* `settings_schema.json` is used to generate the theme settings form
* `setting_data.json` is used data storage for the the theme settings
```bash title="Config Directory"
configs
├── settings_data.json
└── settings_schema.json
```
[See Theme Settings Guide](/docs/storefront/themes/settings)
### Locales [#locales]
The `locales` directory is used to for storefront theme translation json files. The translation files are used in conjunction with the translation template tag to support multiple languages for your storefront.
Translation files should be named according to the ISO 639-1 2 letter language code standard. The default or fallback language should be denoted with a .default in the file name as shown below. See the [Translations guide](/docs/storefront/themes/translations) for more examples on how to localize your theme content.
```bash title="Locale Files Example"
locales
├── en.default.json
├── de.json
├── es.json
├── it.json
└── fr.json
```
[See Translations Guide](/docs/storefront/themes/translations)
### Layouts [#layouts]
The `layouts` directory is used to store base templates that are then extended from in view specific templates, see extends and block template tags for more on template inheritance. See the [`extends` template tag](/docs/storefront/themes/templates/tags#extends--block) for more on template inheritance.
```bash title="Layouts Directory Example"
layouts
└── base.html
```
### Partials [#partials]
The partials directory is used to store reusable which are reusable snippets of code that can be used in tandem with the include template tag for reuse across many templates. See the includes template tag for more on template inheritance with partials.
```bash title="Partials Directory Example"
partials
├── header.html
├── footer.html
└── pagination.html
```
```jinja title="Example Partial Include Another Template"
{% include "partials/footer.html" %}
```
### Templates [#templates]
The templates directory is used to store all templates for a theme, see [URLs and Template Paths](/docs/storefront/themes/templates/urls-and-template-paths) for reference.
```bash title="Templates Directory Example"
templates
├── 403.html
├── 404.html
├── 500.html
├── blog
│ ├── index.html
│ └── post.html
├── cart.html
├── catalogue
│ ├── category.html
│ ├── index.html
│ └── product.html
├── index.html
├── pages
│ └── page.html
├── reviews
│ ├── form.html
│ ├── index.html
│ └── review.html
├── search.html
└── support
├── article.html
├── category.html
└── index.html
```
### Sass [#sass]
The sass directory accepts scss files for use in in a theme. See Theme Kit for more details on local sass compiling.
Sass files are not automatically compiled in the platform and must be compiled to css files locally for use in templates from the assets directory.
### Theme Kit [#theme-kit]
[Theme Kit](https://github.com/NextCommerceCo/theme-kit) is a command line tool for developers to build an maintain storefront themes programmatically, allowing theme developers to:
* Work on theme templates and assets using their local code editor or favorite IDE.
* Use git version control to work on a theme collectively with many theme collaborators.
* Use a pipeline to manage deployments of theme updates.
See the [Theme Kit guide](/docs/storefront/themes/theme-kit) for installation, configuration, and commands. Source: [GitHub](https://github.com/NextCommerceCo/theme-kit).
# Theme Settings (/docs/storefront/themes/settings)
### Introduction [#introduction]
Theme settings are the power behind the dashboard theme editor experience allowing users to customize the look and feel of their storefront without needing to know how to code.
{/* Image: theme-customize-editor.jpg */}
### Theme Settings Location [#theme-settings-location]
Theme settings consist of two JSON files in the `/configs` directory.
```
configs
└── settings_schema.json
└── settings_data.json
```
* `settings_schema.json` - Used to create the settings schema to create settings shown in the dashboard theme editor.
* `settings_data.json` - Used to store theme settings values for access in templates for rendering.
### Using Settings in Templates [#using-settings-in-templates]
Settings are passed to templates settings context variable allowing you to access settings values by their name. See example below of changing the layout by conditionally adding a class based on a radio setting.
```json title="Using Settings in Templates"
{
"General": {
"Settings": [
{
"name": "store_name",
"label": "Store Name",
"help_text": "Public name of your store.",
"type": "text",
"max_length": 250,
"required": 1,
"default": "Store Name"
},
{
"name": "layout",
"label": "Layout Style",
"help_text": "Control the layout style.",
"type": "radio",
"options": [
{
"name": "Boxed",
"value": "boxed"
},
{
"name": "Full Width",
"value": "full"
}
],
"default": "boxed"
}
]
}
}
```
```html title="Template"
Welcome to {{ settings.store_name }}!
Conditionally boxed or full width content
```
### Attribute Reference [#attribute-reference]
| Attribute | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `type` | Yes | Type of form input, see Schema Input Types. |
| `name` | Yes | Name of the setting and key for access in template settings object variable. |
| `label` | Yes | Theme settings form input label. |
| `help_text` | No | Theme settings form input help text that shows below the input. |
| `required` | No | JSON boolean, accepts true or false, false by default. |
| `default` | No | Default value for the setting. |
| `options` | No | List of `key:value` pairs for options. Applicable to radio and select field types for their choices. |
| `max_length` | No | Applicable to `text` field types to limit the length of text input. |
| `max_value` | No | Applicable to number field types to limit the max value. |
| `min_value` | No | Applicable to number field types to set a min value. |
## Schema Input Types [#schema-input-types]
Schema input types map to input fields that will be rendered in the settings form in the dashboard.
### checkbox [#checkbox]
A `checkbox` setting outputs a checkbox field input for use cases such as toggling features on and off.
When accessing a `checkbox` field value in a template, it returns boolean.
```json title="Example checkbox setting"
{
"name": "enable_cookie_msg",
"label": "Enable Cookie Message Pop",
"help_text": "Enable cookie message to site visitors.",
"type": "checkbox",
"default": true
}
```
### color [#color]
A `color` setting outputs a color picker field allowing the user to chose a color for cases such as customizing font and button styles.
When accessing a `color` field value in a template, it returns a hex value as a string.
```json title="Example color setting"
{
"type": "color",
"name": "btn_primary_color",
"label": "Primary Button Color",
"help_text": "Primary color for buttons.",
"default": ""
}
```
### css [#css]
A `css` setting outputs an css code editor allowing the user to add custom css in a nicely formatted editor for cases such as custom css styles.
When accessing a `css` field value in a template, it returns the a string with the css pre-wrapped with `` tags.
```json title="Example css setting"
{
"type": "css",
"name": "custom_css",
"label": "Custom CSS",
"help_text": "Example css input."
}
```
### email [#email]
A `email` setting outputs a email field allowing the user to add validated email text for cases of showing a contact email.
When accessing a `email` field value in a template, it returns a string.
```json title="Example email setting"
{
"type": "email",
"name": "contact_email_address",
"label": "Public contact email address.",
"help_text": "Email to show in site footer.",
"default": ""
}
```
### file [#file]
A `file` setting outputs a file upload input field allowing the user to upload files for scenarios such as a homepage banner background image.
When accessing a `file` field value in a template, it returns a full CDN link to the uploaded file.
```json title="Example file setting"
{
"type": "file",
"name": "logo",
"label": "Store Logo",
"help_text": "Primary logo used throughout the site.",
"default": "uploads/logo.png"
}
```
### html [#html]
A `html` setting outputs an html code editor allowing the user to add custom html in a nicely formatted editor for cases such as code snippets or video embeds.
When accessing a `html` field value in a template, it returns the html content string.
```json title="Example html setting"
{
"type": "html",
"name": "custom_html",
"label": "Custom HTML",
"help_text": "Example html input."
}
```
### image\_picker [#image_picker]
An `image_picker` setting outputs outputs an image picker modal making all uploaded image assets available to select from.
```json title="Example image picker setting"
{
"name": "example_image_picker",
"label": "Example Image Field",
"type": "image_picker",
"required": false
}
```
### menu [#menu]
A `menu` setting field outputs a dropdown select field to choose from the available navigation menus.
When accessing a `menu` setting value in a template, it returns a menu object allowing you to access the menus items. See [menus](/docs/storefront/themes/templates/objects#menus) object details for working with menus in templates.
```json title="Example menu setting"
{
"name": "header_menu",
"label": "Header Menu",
"type": "menu",
"required": true,
"help_text": "Header Menu"
}
```
### multi-select [#multi-select]
A `multi-select` setting outputs a multi-select field that can allows users to select multiple values from a predefined list of options.
When accessing a `multi-select` field value in a template, it returns a list of values that have been saved.
```json title="Example multi-select setting"
{
"type": "select",
"multi-select": true,
"name": "accepted_payment_methods",
"label": "Accepted Payment Methods",
"help_text": "Control which payment methods are shown.",
"options": [
{
"name": "Visa",
"value": "visa"
},
{
"name": "Master Card",
"value": "mastercard"
},
{
"name": "American Express",
"value": "amex"
},
{
"name": "Paypal",
"value": "paypal"
},
{
"name": "Klarna",
"value": "klarna"
}
],
"default": [
"visa",
"mastercard"
]
}
```
### number [#number]
A `number` setting field outputs a standard number input field allowing users to input a number respecting the optionally available min and max values.
When accessing a `number` setting value in a template, it's returned as an integer.
```json title="Example number setting"
{
"type": "number",
"name": "homepage_testimonials_count",
"label": "Homepage Number Testimonials to Show",
"help_text": "Control the number of homepage testimonials to show",
"max_value": 10,
"min_value": 0,
"default": 3
}
```
### page [#page]
A `page` setting field outputs a dropdown select field to choose from the available pages.
When accessing a `page` setting value in a template, it returns a page object. See [page](/docs/storefront/themes/templates/objects#page) object details for working with pages in templates.
```json title="Example page setting"
{
"type": "page",
"name": "page_setting",
"label": "Example Setting Page",
"help_text": "Link to page configured from settings."
}
```
### product [#product]
A `product` setting field outputs a dropdown select field to choose from the available products in the store catalogue.
When accessing a `product` setting value in a template, it returns a product object. See [product](/docs/storefront/themes/templates/objects#product) object details for working with products in templates.
```json title="Example product setting"
{
"name": "hero_product",
"label": "Hero Product",
"type": "product",
"help_text": "Hero product on homepage banner.",
"required": false
}
```
### products [#products]
A `products` setting field outputs a multi-select field to choose from the available products in the store catalogue.
When accessing a `products` setting value in a template, it returns a list of products to iterate through. See [product](/docs/storefront/themes/templates/objects#product) object details for working with products in templates.
```json title="Example products setting"
{
"name": "featured_products",
"label": "Featured Products",
"type": "products",
"help_text": "Featured products for homepage."
}
```
### product\_category [#product_category]
A `product_category` setting field outputs a dropdown select field to choose from the available product categories.
When accessing a `product_category` setting value in a template, it returns a product category object. See [product category](/docs/storefront/themes/templates/objects#product_category) object details for working with product categories in templates.
```json title="Example featured_products setting"
{
"name": "featured_products",
"label": "Featured Product Category",
"type": "product_category",
"help_text": "Featured product category for homepage"
}
```
### product\_categories [#product_categories]
A `product_categories` setting field outputs a multi-select field to choose from the available product categories.
When accessing a `product_categories` setting value in a template, it returns a list of product categories to iterate through. See [product category](/docs/storefront/themes/templates/objects#product_category) object details for working with product categories in templates.
```json title="Example product_categories setting"
{
"name": "top_product_categories",
"label": "Top Product Categories",
"type": "product_categories",
"help_text": "Featured product categories for homepage"
}
```
### radio [#radio]
A `radio` setting outputs a radio option field that can be used in option selection scenarios such as alignment or layout style.
When accessing a `radio` setting value in a template, it's returned as a string.
```json title="Example radio setting"
{
"type": "radio",
"name": "layout",
"label": "Layout Style",
"help_text": "Control the layout style.",
"options": [
{
"name": "Boxed",
"value": "boxed"
},
{
"name": "Full Width",
"value": "full"
}
],
"default": "boxed"
}
```
### range [#range]
A `range` setting outputs a slider that can be used to a varying numerical value such as font size, number of columns or opacity.
When accessing a `range` setting value in a template, it's returned as an integer.
```json title="Example range setting"
{
"type": "range",
"name": "slider_range_field",
"label": "Headings font size",
"help_text": "",
"min": 0,
"max": 50,
"step": 1,
"unit": "px",
"required": true,
"default": 25
}
```
### richtext [#richtext]
A `richtext` setting displays a WYSIWYG editor with basic text formatting options allowing users to input and format text content.
When accessing a `richtext` setting value in a template, it's returned as a string.
```json title="Example richtext setting"
{
"type": "richtext",
"name": "richtext_content",
"label": "Description Content",
"help_text": "Example rich text input field."
}
```
### select [#select]
A `select` setting outputs a dropdown option select field that can be used in option selection scenarios such as alignment or layout style.
When accessing a `select` setting value in a template, it's returned as a string.
```json title="Example select setting"
{
"type": "select",
"name": "header_style",
"label": "Header Style",
"help_text": "Choose header layout style.",
"options": [
{
"name": "Full Width",
"value": "full"
},
{
"name": "Boxed",
"value": "boxed"
},
{
"name": "Overlay",
"value": "overlay"
}
],
"default": "full"
}
```
### text [#text]
A `text` setting outputs a single line input field that can be used in scenarios such as the banner heading text on the homepage.
When accessing a `text` setting value in a template, it's returned as a string.
```json title="Example text setting"
{
"type": "text",
"name": "store_name",
"label": "Store Name",
"help_text": "Public name for your store.",
"max_length": 250,
"required": true,
"default": "My New Store Name"
}
```
### textarea [#textarea]
A `textarea` setting outputs a multi-line textarea input field that can be used in scenarios such as the banner sub-heading text on the homepage.
When accessing a `textarea` setting value in a template, it's returned as a string.
```json title="Example textarea setting"
{
"type": "textarea",
"name": "description",
"label": "Description",
"help_text": "Example input textarea",
"default": "Example long multi-line sub-heading text for the banner."
}
```
### url [#url]
A `url` setting outputs an input url type field that accepts fully qualified urls that can be used for relative paths or external url links.
When accessing a `url` setting value in a template, it's returned as a string.
```json title="Example url setting"
{
"type": "url",
"name": "social_link",
"label": "Social Media Link",
"help_text": "Link to your social media page.",
"default": ""
}
```
# Theme Kit (/docs/storefront/themes/theme-kit)
[Theme Kit](https://github.com/NextCommerceCo/theme-kit) is a command line tool for developers to build and maintain storefront themes programmatically, allowing theme developers to:
* Work on theme templates and assets using their local code editor or favorite IDE.
* Use git version control to work on a theme collectively with many theme collaborators.
* Use a pipeline to manage deployments of theme updates.
[See Full Instructions on Github](https://github.com/NextCommerceCo/theme-kit) or [Install Theme Kit from PyPi](https://pypi.org/project/next-theme-kit/)
### Installation [#installation]
Theme Kit is a python package available on [PyPi](https://pypi.org/project/next-theme-kit/)
If you already have `python` and `pip`, install with the following command:
```bash title="Installation"
pip install next-theme-kit
```
#### Mac OSX Requirements [#mac-osx-requirements]
See how to install `python` and `pip` with [HomeBrew](https://docs.brew.sh/Homebrew-and-Python#python-3x). Once you have completed this step you can install using the `pip` instructions above.
#### Windows Requirements [#windows-requirements]
* **Option 1 (Recommended)** - Windows 10 and above feature WSL (Windows Subsystem for Linux) which provides a native Linux environment, see how to [Install WSL with Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install). Once you have installed WSL, follow the [best practice guides to configure and use with VS Code](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) and then follow the `pip` instructions above to install Theme Kit.
* **Option 2** - Installing `python` in Windows natively can be done with through the [Windows App Store](https://apps.microsoft.com/store/detail/python-39/9P7QFQMJRFP7?hl=en-us\&gl=us). Recommend using [Windows Powershell](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D?hl=en-us\&gl=us). This route is a little more tricky and some knowledge on how to manage python in windows will be required.
**Use Python Virtual Environments** - For Mac, Windows, and Linux, it's a best practice to use a Python Virtual Environment to isolate python packages and dependencies to reduce potential conflicts or errors, [more on creating a Python Virtual Environment](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/).
### Setup [#setup]
Connect `ntk` to a store in three steps.
#### 1. Create the API Key [#1-create-the-api-key]
Store authentication uses [OAuth 2.0](https://auth0.com/intro-to-iam/what-is-oauth-2/) and requires creating a store OAuth App with the `themes:read` and `themes:write` permissions.
1. In the Storefront admin, go to **Settings > API Access**.
2. Click **Create App**.
3. Give the app a name and assign a user.
4. In the **Permissions** tab, enable `themes:read` and `themes:write`.
5. **Save**. Copy the generated API key — you will need it in the next step.
#### 2. Configure Theme Kit [#2-configure-theme-kit]
`ntk` reads its connection settings from two places: command flags (`--apikey`, `--store`, `--theme_id`) and the `config.yml` file in your theme directory. You do not need to create `config.yml` by hand — `ntk checkout` and `ntk init` write it for you, and after that commands run without flags:
```yaml title="config.yml (written by ntk checkout / ntk init)"
development:
apikey:
store: https://{store}.29next.store
theme_id:
```
Keep the API key out of source control. Do not commit `config.yml` to git if it contains the key.
`config.yml` supports multiple environments. Commands use the `development` entry by default; pass `-e` / `--env` to target another environment (e.g. `ntk push --env=production`). The `[development]` prefix in command output is the active environment.
#### 3. Connect to a Theme [#3-connect-to-a-theme]
Work from a copy of an existing theme rather than an empty directory — a complete theme is the reference for the required directories, templates, and settings.
**Work on a theme already on the store** — `ntk checkout` downloads the theme into your current directory and writes `config.yml`:
```bash
ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store"
```
**Add a new theme to the store** — start from a copy of an existing theme, such as the [Spark](https://github.com/NextCommerceCo/spark) starter theme, then register it as a new theme with `ntk init` and upload the files with `ntk push`:
```bash
ntk init --name="" --apikey="" --store="https://{store}.29next.store"
ntk push
```
### Usage [#usage]
With the package installed, you can now use the commands inside your theme directory and work on a storefront theme.
| Command | Description |
| -------------- | --------------------------------------------------------------- |
| `ntk init` | Initialize a new theme |
| `ntk list` | List all available themes |
| `ntk checkout` | Checkout an existing theme |
| `ntk pull` | Download existing theme or theme file |
| `ntk push` | Push current theme state to store |
| `ntk watch` | Watch for local changes and automatically push changes to store |
| `ntk sass` | Process sass to css, see [Sass Processing](#sass-processing) |
#### Browse Store Themes [#browse-store-themes]
To see what themes exist on the store, run `ntk list` to print the theme ID and name of each, with the active theme marked.
```bash
ntk list
```
Output looks like:
```
[development] Available themes:
[development] [42] Spring Launch
[development] [43] Holiday Promo (Active)
```
If you do not have a `config.yml`, also pass `--apikey` and `--store`.
#### Work on an Existing Theme [#work-on-an-existing-theme]
To start working on a theme that already exists on the store, `ntk checkout` downloads it into your directory and writes `config.yml` with the theme ID.
```bash
ntk checkout --theme_id=
```
`--theme_id` / `-t` is required. If you do not have a `config.yml`, also pass `--apikey` and `--store`:
```bash
ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store"
```
`ntk checkout` differs from `ntk pull` in one way: `checkout` writes `config.yml` so the directory is ready for subsequent `ntk push` / `ntk watch` runs; `pull` downloads the same files without writing `config.yml`.
#### Add a New Theme to the Store [#add-a-new-theme-to-the-store]
`ntk init` registers your current directory as a new theme on the store and writes a `config.yml`. It does not download or scaffold any files — run it inside an existing theme codebase, then `ntk push` to upload the files.
Building a theme from an empty directory is not advised. Start from a copy of a complete theme — the [Spark](https://github.com/NextCommerceCo/spark) starter theme (Tailwind CSS), the older Bootstrap-based [Intro Bootstrap](https://github.com/NextCommerceCo/intro-bootstrap) or an existing theme from your store via [`ntk checkout`](#work-on-an-existing-theme).
```bash
ntk init --name=""
```
`--name` / `-n` is required. If you do not have a `config.yml` yet, also pass `--apikey` and `--store`:
```bash
ntk init --name="" --apikey="" --store="https://{store}.29next.store"
```
On success, `ntk init` logs the new theme ID and name, and persists the theme ID into `config.yml` so subsequent commands can omit `--theme_id`.
#### Sync Files to the Store [#sync-files-to-the-store]
To sync files between your local directory and the store, use `ntk push` to upload and `ntk pull` to download. Both upload or download the whole theme by default, and both accept file paths as positional arguments to limit the operation to specific files.
File paths are relative to the theme root. `ntk push` only uploads files inside the theme directories (`assets`, `configs`, `layouts`, `locales`, `partials`, `sass`, `templates`) with valid theme file extensions. A path outside those directories is skipped silently. `ntk pull` downloads the `checkout` directory, but `ntk push` skips it because the store does not accept checkout uploads.
```bash
ntk push templates/index.html
```
```bash
ntk push templates/index.html assets/main.css
```
```bash
ntk pull templates/index.html
```
```bash
ntk pull templates/index.html assets/main.css
```
#### Watch for File Changes [#watch-for-file-changes]
`ntk watch` monitors your theme directory and automatically pushes changed files to the store. Use it while you develop — save a file and the change is uploaded moments later.
```bash
ntk watch
```
On start, `ntk watch` logs the store, theme ID, a preview-theme URL, and the directory it is watching. Press `Ctrl + C` to stop.
After each upload, open the affected route on `https://{store}.29next.store` and verify the served HTML, expected assets, and behavior. Do not use a mapped public storefront domain to decide whether a push succeeded. Check the public domain only after network-domain verification.
Deletes sync too — deleting a local file while `ntk watch` is running deletes that file from the theme on the store.
`ntk watch` watches the current directory tree (subdirectories included) and only uploads files with valid theme extensions. It does not accept file arguments. To scope changes to specific files, run `ntk push` with file paths instead.
#### Sass Processing [#sass-processing]
Theme kit includes support for Sass processing via [Python Libsass](https://sass.github.io/libsass-python/). Sass processing includes support for variables, imports, nesting, mixins, inheritance, custom functions, and more.
Sass processing is only supported on local, files in the `sass` directory are uploaded to your store for storage but cannot be edited in the store theme editor.
**How it works**
1. Put `scss` files in top level `sass` directory.
2. Run `ntk sass` or `ntk watch` to process theme `sass` files.
3. Top level `scss` files will be processed to `css` files in the asset directory with the same name.
**Example Theme with Sass Structure**
```text title="Sass Processing"
├── assets
│ ├── main.css // reference this asset file in templates
├── sass
│ ├── _base.scss
│ ├── _variables.scss
│ └── main.scss // processed to assets/main.css
```
# Translations (/docs/storefront/themes/translations)
Theme templates can be fully localized with translations so that your store visitors are shown content in their local language. Use the t (translation) tag in your templates to access string translations in the locale files. Learn more about the [t tag](/docs/storefront/themes/templates/tags#t) and theme Locale files.
### Using Translations in Practice [#using-translations-in-practice]
The `t` tag accepts an initial argument that is the key reference to the translation string in a locale file.
```jinja title="Using Translations in Practice"
{% t 'customer.orders.order_history' %}
```
```json title="en.default.json"
{
"orders": {
"order_history": "Order History"
}
}
```
```html title="Result"
Order History
```
### Passing Variable Arguments to Translations [#passing-variable-arguments-to-translations]
You can pass multiple named arguments to translations through the `t` tag used in the translation.
```jinja title="Passing Variable Arguments to Translations"
{% t 'customer.profile.welcome_msg' with name=request.user.first_name %}
```
```json title="Passing Variable Arguments to Translations"
{
"customer": {
"welcome_message": "Hi {{ name }}, welcome to your account!"
}
}
```
```html title="Result"
Hi John, welcome to your account!
```
### Pluralization Support [#pluralization-support]
The t tag accepts two arguments for pluralization:
* Pass the count argument with a value for cardinal pluralization, ie 1, 2, 3, 4.
* Pass the index argument with a value for ordinal pluralization, ie First, Second, Third, Forth.
Pluralization rules follow [Unicode CLDR](https://github.com/unicode-org/cldr) specification, available keys include:
* `one`
* `other`
* `two`
* `zero`
* `few`
* `many`
#### Cardinal Pluralization [#cardinal-pluralization]
Cardinal pluralization can be used to display different translations based on the value passed to `count`.
```jinja title="Cardinal Pluralization"
{% t 'customer.notifcations.total' with count=notification.count %}
```
```json title="Cardinal Pluralization"
{
"customer": {
"notifcations": {
"one": "You have {{ count }} notification.",
"other": "You have {{ count }} notifications.",
"zero": "You don't have any notifications."
}
}
}
```
```html title="Result"
{/* Count is 1 */}
You have 1 notification.
{/* Count is 3 */}
You have 3 notifications.
{/* Count is 0 */}
You don't have any notifications.
```
#### Ordinal Pluralization [#ordinal-pluralization]
Ordinal pluralization can be used to display different translations based on the index value to display the ordering of an object.
```jinja title="Ordinal Pluralization"
{% t 'customer.orders.orders_msg' with index=customer.orders.count %}
```
```json title="Ordinal Pluralization"
{
"customer": {
"orders_msg": {
"one": "Congrats on your {{ index }}st order!",
"two": "Congrats on your {{ index }}nd order!",
"few": "Congrats on your {{ index }}rd order!",
"other": "Congrats on your {{ index }}th order!"
}
}
}
```
```html title="Result"
{/* Count is 1 */}
Congrats on your 1st order!
{/* Count is 2 */}
Congrats on your 2nd order!
{/* Count is 3 */}
Congrats on your 3rd order!
{/* Count is 4 */}
Congrats on your 4th order!
```
# Affirm Admin API Guide (/docs/admin-api/guides/payment-methods/affirm)
**Affirm** is a fully integrated Buy Now Pay Later (BNPL) payment method via NEXT Payments and Stripe, supported both in the storefront checkout, and via the Admin API.
Affirm transactions send the customer through an Affirm redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get Affirm set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Affirm using the orders\_create API method, you must specify the `payment_method=affirm` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Affirm"
{
"payment_method": "affirm",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use an Affirm account connected to a specific gateway.
### Redirect Customer to Affirm [#redirect-customer-to-affirm]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on Affirm.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Upsells are not supported with Affirm payments.
# Afterpay Admin API Guide (/docs/admin-api/guides/payment-methods/afterpay)
**Afterpay** is a fully integrated Buy Now Pay Later (BNPL) payment method via Stripe, supported in the storefront checkout and through the Admin API.
Afterpay transactions send the customer through an Afterpay redirect flow, with the resulting order information returned to your application. Before using Afterpay, activate it on the Stripe account connected to the gateway. In the United Kingdom the same method is branded **Clearpay**.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating an order with the `orders_create` API method, specify `payment_method=afterpay` and provide a `payment_return_url`. The `payment_return_url` is your endpoint that receives a POST request containing the final order data.
```json title="Payment Details for Order with Afterpay"
{
"payment_method": "afterpay",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use an Afterpay account connected to a specific gateway.
### Redirect Customer to Afterpay [#redirect-customer-to-afterpay]
The order response provides a `payment_complete_url`. Redirect the customer to this URL to complete the payment with Afterpay.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
One-click post-purchase upsells are not supported with Afterpay payments. Afterpay authorizes a single sale and the payment method cannot be reused for a later merchant-initiated charge.
### Recurring [#recurring]
Afterpay cannot be used as the payment method for an order with subscription items.
# Apple Pay Admin API Guide (/docs/admin-api/guides/payment-methods/apple-pay)
**Apple Pay** is a fully integrated payment app, supported both in the storefront checkout, and via the Admin API. Apple Pay transactions process the customer through the Apple Pay payment flow, with the resulting order information provided back to your application. Below are the steps needed to get Apple Pay set up and working on the Admin API.
For custom checkouts using the Admin API, there are two flows available -- the standard method where a user enters their shipping address, chooses products, and then checks out via Apple Pay; and the "One-Click" method, where the user is not required to enter shipping information before being redirected to Apple Pay checkout.
Your store must have a Apple Pay setup and enabled with a gateway to use the Apple Pay payment method. The user device must also be an Apple Device with Touch ID enabled. See more on [displaying Apple Pay buttons](https://developer.apple.com/documentation/apple_pay_on_the_web/displaying_apple_pay_buttons_using_css) or the [Apple Pay Demo](https://applepaydemo.apple.com/).
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Apple Pay, you’ll need to specify the `payment_method=apple_pay` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Apple Pay"
{
"payment_method": "apple_pay",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
### Redirect Customer to Payment Complete URL [#redirect-customer-to-payment-complete-url]
The response when creating the order will provide a payment\_complete\_url. Your application should redirect the customer to this URL for completing the payment on the store's Apple Pay Checkout page.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": "https:///checkout/apple-pay//"
}
```
### Receiving Order Data [#receiving-order-data]
# Bancontact Admin API Guide (/docs/admin-api/guides/payment-methods/bancontact)
**Bancontact** is a fully integrated payment method that is supported both in the storefront checkout, and via the Admin API.
Bancontact transactions send the customer through a Bancontact redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get Bancontact set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Bancontact using the orders\_create API method, you must specify the `payment_method=bancontact` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Bancontact"
{
"payment_method": "bancontact",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a Bancontact account connected to a specific gateway.
### Redirect Customer to Bancontact [#redirect-customer-to-bancontact]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on Bancontact.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Upsells are not supported with Bancontact payments.
# Bankcard (/docs/admin-api/guides/payment-methods/bankcard)
Bankcard is the core payment method on the Admin API. Cards are charged by passing `payment_method: card_token` with a tokenized card in `payment_details`. Tokenizing the card with our iFrame payment form keeps sensitive card data off your servers and reduces your PCI compliance scope.
This guide covers the full bankcard flow:
* [Gateway routing](#gateway-routing): route charges across one or more payment gateways.
* [Card tokenization](#card-tokenization-iframe): get a `card_token` with the iFrame payment form.
* [3D Secure (3DS2)](#3d-secure-3ds2): process cards through an authentication flow.
## Order payment payload [#order-payment-payload]
Bankcard orders set `payment_method` to `card_token` and pass the tokenized card (and any optional fields) in `payment_details`:
```json title="Bankcard Payment Detail"
"payment_method": "card_token",
"payment_details": {
"card_token": "", // from the iFrame (see below)
"save_card": true, // retain card for future charges (default true)
"statement_descriptor": "BRANDNAME", // optional, up to 22 chars
"payment_gateway": 12, // optional, route to a specific gateway
"payment_gateway_group": 3, // optional, route to a gateway group
"payment_return_url": "" // required for 3DS (see below)
}
```
| Field | Type | Description |
| ----------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `card_token` | string | Tokenized card produced by the iFrame payment form. |
| `save_card` | boolean | Retain the card for future charges (subscriptions, one-click upsells). Defaults to `true`. |
| `statement_descriptor` | string | Custom bank-statement descriptor (≤ 22 alphanumeric chars, spaces, and `& , . - #`). |
| `payment_gateway` | integer | Optional. Charge a specific gateway by id. See [Gateway routing](#gateway-routing). |
| `payment_gateway_group` | integer | Optional. Charge a gateway group by id. See [Gateway routing](#gateway-routing). |
| `payment_return_url` | string (uri) | Required for 3DS. Your endpoint that receives the final order data. See [3D Secure](#3d-secure-3ds2). |
For the complete order request (lines, user, addresses, shipping), see the [External Checkout Flow](/docs/admin-api/guides/external-checkout) guide. This guide focuses on the bankcard-specific payment detail.
## Gateway Routing [#gateway-routing]
By default, bankcard charges route through your store's configured payment gateway. For stores with **more than one gateway**, you can optionally pin a charge to a specific gateway or gateway group by passing one of these fields in `payment_details`:
| Field | Type | Routes to |
| ----------------------- | ------- | ----------------------------------------------------------------------- |
| `payment_gateway` | integer | A **single gateway** by its `id`. |
| `payment_gateway_group` | integer | A **gateway group** by its `id`. The platform selects a member gateway. |
```json title="Route to a specific gateway or group"
"payment_method": "card_token",
"payment_details": {
"card_token": "",
"payment_gateway": 12 // OR "payment_gateway_group": 3
}
```
Pass either `payment_gateway` or `payment_gateway_group`, not both. If you pass neither, the store's default routing applies.
### Get gateway and group ids [#get-gateway-and-group-ids]
The ids are integers from the Payments API:
* Gateways: [`gatewaysList`](/docs/admin-api/reference/payments/gatewaysList) and [`gatewaysRetrieve`](/docs/admin-api/reference/payments/gatewaysRetrieve)
* Gateway groups: [`gatewayGroupsList`](/docs/admin-api/reference/payments/gatewayGroupsList) and [`gatewayGroupsRetrieve`](/docs/admin-api/reference/payments/gatewayGroupsRetrieve)
A gateway group also exposes its supported `available_currencies`, `available_payment_methods`, and `card_types`, so you can pick the right group for a given order.
### When to route [#when-to-route]
Most stores let the platform route automatically. Pass an explicit gateway or group when you need to:
* Load balance volume across several gateways or processors.
* Send orders to the gateway that supports a given currency or market.
* Fail over to a backup gateway, or use a group so a soft decline can be retried on another gateway in the group.
Group membership, distribution weighting, and soft-decline retries are configured per gateway in your store dashboard. The API only selects among what is already configured.
## Card Tokenization (iFrame) [#card-tokenization-iframe]
Tokenize cards in our iFrame payment form before you send them to the Admin API. Your checkout never handles raw card data, which keeps it out of PCI scope.
Try the [Demo](https://nextcommerceco.github.io/demo-iframe-payment-form/). The source is on [Github](https://github.com/NextCommerceCo/demo-iframe-payment-form/blob/main/index.html).
`payment.js` is a script hosted by Next Commerce. It exposes the `NextPayment` class, which mounts the card number and security code fields in iFrames, validates them, and returns a token. You can style the fields to match the rest of your form.
### Setup [#setup]
You need your store's Payments Environment Key. Find it under **Settings > Payments**, or read `payments.environment_key` from the [Store Detail](/docs/admin-api/reference/store/storeDetail) endpoint.
**Card tokenization steps**
1. Add `payment.js` with your environment key.
2. Add the form. Card number and security code are empty containers, the rest are normal inputs.
3. Create a `NextPayment` and assign callbacks.
4. Call `submit()` from your form handler.
5. Read the token in `onTokenized`.
```html title="Add payment.js"
```
`env_key` is required. A key that matches no store still returns the script, but without credentials, so every tokenization fails.
Add the form. `NextPayment` mounts iFrames into the two empty `div` elements. The other fields are normal inputs.
```html title="Example Payment Form HTML"
```
Create the instance. The constructor starts loading the iFrames at once, so assign callbacks right after it.
```js title="Initialize NextPayment"
const submitButton = document.querySelector('#payment-form button[type="submit"]');
const payment = new NextPayment({
numberEl: "id_card_number",
cvvEl: "id_card_cvv",
});
payment.onReady = () => {
submitButton.removeAttribute('disabled');
payment.setFocus("number");
};
```
`numberEl` and `cvvEl` are element ids, not selectors. Keep the submit button disabled until `onReady` fires.
#### Configuration options [#configuration-options]
| Option | Required | Description |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `numberEl` | Yes | Container element id where the card number iFrame is mounted. |
| `cvvEl` | Yes | Container element id where the security code iFrame is mounted. |
| `numberFormat` | No | One of `prettyFormat` (default), `plainFormat`, or `maskedFormat`. |
| `labels` | No | `{ number, cvv }` accessible labels. Defaults to `Card Number` and `Security Code`. |
| `placeholder` | No | `{ number, cvv }` placeholder text. Defaults to `Card Number` and `CVV`. |
| `titles` | No | `{ number, cvv }` title attributes. Defaults to `Enter your card number` and `Enter your Security Code`. |
| `styling` | No | `{ number, cvv, placeholder }` objects of camelCase CSS properties. See [Style iFrame Fields](#style-iframe-fields). |
Both fields are always required and use input type `text`. Card type detection is covered below.
### Style iFrame Fields [#style-iframe-fields]
Pass a `styling` object so the iFrame fields match your native inputs. Give it the same font, size, weight, line height, colour, and padding. Each value is an object of camelCase CSS properties, not a CSS string. `placeholder` styles the placeholder text in both fields. The demo copies Bootstrap's `.form-control` values.
```js title="Example form customization"
const formControlStyle = {
width: '100%',
padding: '.375rem .75rem',
fontSize: '1rem',
fontWeight: '400',
lineHeight: '1.5',
color: '#212529',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
};
const payment = new NextPayment({
numberEl: "id_card_number",
cvvEl: "id_card_cvv",
styling: {
number: formControlStyle,
cvv: formControlStyle,
placeholder: { color: 'rgba(33, 37, 41, 0.75)' },
},
});
```
Serve the page over HTTP or HTTPS. Opened from disk, the page has a `null` origin, the SDK cannot reach the iFrames, `onReady` never fires, and the fields keep the browser's default styling.
CSS `:focus` cannot reach inside an iFrame. Use `onFieldStateChange` to style the container on focus and blur. The same payload carries the detected card type, so you can show a brand icon as the customer types.
```html title="Card type icon next to the card number field"
```
```js title="Focus styling and card type detection with onFieldStateChange"
const cardTypeIcons = {
visa: 'img/cardbrands/visa.svg',
master: 'img/cardbrands/mastercard.svg',
american_express: 'img/cardbrands/amex.svg',
discover: 'img/cardbrands/discover.svg',
diners_club: 'img/cardbrands/diners_club.svg',
jcb: 'img/cardbrands/jcb.svg',
maestro: 'img/cardbrands/maestro.svg',
dankort: 'img/cardbrands/dankort.svg',
};
payment.onFieldStateChange = (payload) => {
const {action, field, cardType} = payload;
const cardTypeEl = document.getElementById('id_card_type');
if (cardTypeIcons[cardType]) {
cardTypeEl.src = cardTypeIcons[cardType];
cardTypeEl.hidden = false;
} else {
cardTypeEl.hidden = true;
}
const fieldEl = document.querySelector(`#id_card_${field}`);
if (!fieldEl) return;
if (action === "focus") {
fieldEl.classList.add('active');
}
if (action === "blur") {
fieldEl.classList.remove('active');
}
}
```
**`onFieldStateChange` payload**
| Field | Description |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `field` | `number` or `cvv`. |
| `action` | One of `focus`, `blur`, `input`, `mouseover`, `mouseout`, `enter`, `escape`, `tab`, or `shiftTab`. |
| `cardType` | Detected brand: `visa`, `master`, `american_express`, `discover`, `diners_club`, `jcb`, `maestro`, `dankort`, and others. Empty until a brand matches. |
| `numberLength`, `cvvLength` | Digits entered in each field. |
| `validNumber`, `validCvv`, `luhnValid` | Current validity of each field. |
| `focused`, `hovered` | Whether the field is focused or hovered. `hovered` is only present on mouse actions. |
The demo's icons are in [img/cardbrands](https://github.com/NextCommerceCo/demo-iframe-payment-form/tree/main/img/cardbrands). The icon updates on every `input` action.
### Tokenize Card [#tokenize-card]
Call `submit()` from your form handler with the cardholder values as `formData`.
```js title="Tokenize Card on Form Submit"
function submitPaymentForm() {
submitButton.setAttribute('disabled', true);
submitButton.textContent = 'Processing...';
let full_name = document.querySelector('#id_cardholder_name').value;
let month = document.querySelector('#id_card_exp_month').value;
let year = document.querySelector('#id_card_exp_year').value;
const formData = {
full_name,
month,
year
};
payment.submit(formData);
};
```
`submit()` returns at once. The result arrives in `onTokenized` or `onError`. First, `submit()` checks `month`, `year`, and `full_name`. If any fail, it reports them in `onValidation` and stops. Neither `onTokenized` nor `onError` fires for that attempt.
Always assign `onValidation`. Without it, `submit()` skips the check and sends the data anyway.
**`formData` fields**
| Field | Required | Description |
| ----------- | -------- | --------------------------------------------------------------------- |
| `full_name` | Yes | Cardholder name. `first_name` and `last_name` do not replace it. |
| `month` | Yes | Expiry month, `1` to `12`. |
| `year` | Yes | Four-digit expiry year, such as `2028`. Two-digit years are rejected. |
### Retrieve Card Token [#retrieve-card-token]
`onTokenized` fires with the result, including the payment method token and card details. Send the token to your backend and pass it to the Admin API as `card_token` when you create the order.
```js title="Retrieve Card Token"
payment.onTokenized = (result) => {
const payment_method = result.tokenResponse.payment_method;
console.log('Payment Method Data:', payment_method);
document.getElementById("card-token").textContent = payment_method.token;
submitButton.textContent = 'Pay Now';
submitButton.removeAttribute('disabled');
};
```
There are two tokens. `tokenResponse.token` is the transaction. `tokenResponse.payment_method.token` is the payment method. Pass the second one as `card_token`.
```json title="Example onTokenized payload"
{
"message": "Token generated",
"tokenResponse": {
"token": "7Q88WFXX2C91YTWQM24GKAEV4A",
"succeeded": true,
"transaction_type": "AddPaymentMethod",
"state": "succeeded",
"message": "Succeeded!",
"payment_method": {
"token": "01M06YP7T3G2RV5ZJ5H1DSTXBD",
"storage_state": "cached",
"test": true,
"last_four_digits": "1111",
"first_six_digits": "411111",
"card_type": "visa",
"month": 11,
"year": 2026,
"full_name": "a t",
"payment_method_type": "credit_card",
"fingerprint": "474713f866e20b9c7f143ad7643364be37e0",
"number": "XXXX-XXXX-XXXX-1111"
}
}
}
```
### Error Handling [#error-handling]
Two callbacks report errors. `onValidation` fires when `submit()` is blocked by a bad field. `onError` fires when the tokenization request fails. Nothing fires while the customer is typing.
```js title="Error Handling"
function fieldSelectorForAttribute(attribute) {
if (attribute === 'number') return '#id_card_number';
if (attribute === 'cvv') return '#id_card_cvv';
if (attribute === 'month') return '#id_card_exp_month';
if (attribute === 'year') return '#id_card_exp_year';
if (attribute === 'full_name') return '#id_cardholder_name';
return null;
}
function setFieldInvalid(selector, message) {
let el = document.querySelector(selector);
if (!el) return;
el.classList.add('is-invalid');
}
payment.onValidation = (payload) => {
document.querySelectorAll('#payment-form .is-invalid').forEach(el => el.classList.remove('is-invalid'));
(payload.errors || []).forEach(err => {
let selector = fieldSelectorForAttribute(err.attribute);
if (selector) setFieldInvalid(selector, err.message);
});
submitButton.removeAttribute('disabled');
submitButton.textContent = 'Pay Now';
};
payment.onError = (error) => {
console.log('on error: ', error);
if (typeof error === 'string') {
alert(error);
} else if (error.errors && Array.isArray(error.errors)) {
error.errors.forEach(err => {
let selector = fieldSelectorForAttribute(err.attribute);
if (selector) setFieldInvalid(selector, err.message);
});
} else if (error.message) {
alert(error.message);
}
submitButton.removeAttribute('disabled');
submitButton.textContent = 'Pay Now';
};
```
#### onValidation [#onvalidation]
`payload.errors` is an array of objects with `attribute`, `key`, and `message`. Two sources feed it, both from `submit()`. `NextPayment` checks `month`, `year`, and `full_name` first. If those pass, the iFrames check `number` and `cvv`. Each firing covers only its own fields.
| `attribute` | Validated by |
| ----------- | -------------------------- |
| `number` | The card number iFrame. |
| `cvv` | The security code iFrame. |
| `month` | `submit()` pre-validation. |
| `year` | `submit()` pre-validation. |
| `full_name` | `submit()` pre-validation. |
```json title="Example Validation Errors"
[
{
"attribute": "month",
"key": "errors.invalid",
"message": "Expiry month is required"
},
{
"attribute": "full_name",
"key": "errors.invalid",
"message": "Cardholder name is required"
}
]
```
#### onError [#onerror]
The payload varies by cause, so check its type first.
| Error source | Payload |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Card number or security code rejected, or submit throttled | A string message, for example `Invalid CVV`. |
| Tokenization API failure | An object with an `errors` array. Each entry has `attribute`, `key`, and `message`. |
| Other SDK failures | An object with a `message` field. |
When the iFrame rejects the number or security code, `onValidation` fires first, then `onError` repeats it as a string.
### Methods [#methods]
#### setFocus [#setfocus]
Focus one of the iFrame fields, for example the card number on page load or a field with an error. Does nothing before `onReady`.
```js title="setFocus"
payment.setFocus("number");
```
**Arguments**
| Name | Description |
| ----- | ----------------------------- |
| field | `number`, `cvv`, or `iframe`. |
#### destroy [#destroy]
Remove the iFrames. Call it before you discard an instance, such as when a modal closes, so the old instance stops reacting to events.
```js title="destroy"
payment.destroy();
```
### Test Cards [#test-cards]
Use the [Test Gateway](/docs/admin-api/guides/testing-guide#test-gateway) card numbers with any name, future expiry, and security code. The demo uses `4111111111111111`.
### Migrating from Spreedly iFrame v1 [#migrating-from-spreedly-iframe-v1]
Older integrations load `iframe-v1.min.js` and call `Spreedly` directly. Replace that script with `payment.js` from [Setup](#setup) and map the calls:
| Spreedly iFrame v1 | NextPayment |
| -------------------------------- | ------------------------------------------------------ |
| `Spreedly.init(envKey, options)` | `new NextPayment(options)` |
| `Spreedly.on('ready')` | `payment.onReady` |
| `Spreedly.setStyle()` | `styling` option, camelCase objects |
| `Spreedly.setPlaceholder()` | `placeholder` option |
| `Spreedly.setNumberFormat()` | `numberFormat` option |
| `Spreedly.setFieldType()` | Not configurable, always `text` |
| `Spreedly.transferFocus()` | `payment.setFocus()` |
| `Spreedly.tokenizeCreditCard()` | `payment.submit()` |
| `Spreedly.on('paymentMethod')` | `payment.onTokenized`, token at `payment_method.token` |
| `Spreedly.on('errors')` | `payment.onValidation` and `payment.onError` |
## 3D Secure (3DS2) [#3d-secure-3ds2]
3DS2 payments are fully supported via the Admin API to process the customer through an authentication flow, with the final transaction information and results provided back to your application.
Your store must have a 3DS2-enabled gateway to process 3DS2 transactions.
### API payment redirect flow [#api-payment-redirect-flow]
### Create the order [#create-the-order]
When creating an order using a 3DS2-enabled gateway, use `payment_method=card_token` and provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with 3DS2 Payment"
"payment_method": "card_token",
"payment_details": {
"card_token": "",
"payment_return_url": "",
"payment_gateway": 12, // optional
"payment_gateway_group": 3 // optional
}
```
You can optionally provide a `payment_gateway` or `payment_gateway_group` (see [Gateway routing](#gateway-routing)) to authenticate against a specific gateway configured in the store.
### Redirect to the payment complete URL [#redirect-to-the-payment-complete-url]
The order response provides a `payment_complete_url`. Redirect the customer to this URL to complete the payment authentication.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": "https:///payments/3ds-auth/?token="
}
```
### Receive order data [#receive-order-data]
# Google Pay Admin API Guide (/docs/admin-api/guides/payment-methods/google-pay)
**Google Pay** is a fully integrated payment app, supported both in the storefront checkout, and via the Admin API. Google Pay transactions process the customer through the Google Pay payment flow, with the resulting order information provided back to your application. Below are the steps needed to get Google Pay set up and working on the Admin API.
For custom checkouts using the Admin API, there are two flows available -- the standard method where a user enters their shipping address, chooses products, and then checks out via Google Pay; and the "One-Click" method, where the user is not required to enter shipping information before being redirected to Google Pay checkout.
Your store must have a Google Pay setup and enabled with a gateway to use the Google Pay payment method and the user device must use Chrome browser or Android with Google Pay setup.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Google Pay, you’ll need to specify the `payment_method=google_pay` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Google Pay"
{
"payment_method": "google_pay",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
To test Google Pay as a payment method, you can use the `test` gateway with your real credit card in your Google Pay account, your card will not be charged.
### Redirect Customer to Payment Complete URL [#redirect-customer-to-payment-complete-url]
The response when creating the order will provide a payment\_complete\_url. Your application should redirect the customer to this URL for completing the payment on the store's Google Pay Checkout page.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": "https:///checkout/google-pay//"
}
```
### Receiving Order Data [#receiving-order-data]
# iDEAL Admin API Guide (/docs/admin-api/guides/payment-methods/ideal)
**iDEAL** is a fully integrated payment method that is supported both in the storefront checkout, and via the Admin API.
iDEAL transactions send the customer through a iDEAL redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get iDEAL set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using iDEAL using the orders\_create API method, you must specify the `payment_method=ideal` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with iDEAL"
{
"payment_method": "ideal",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a iDEAL account connected to a specific gateway.
### Redirect Customer to iDEAL [#redirect-customer-to-ideal]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on iDEAL.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Upsells are not supported with iDEAL payments.
# Payment Methods (/docs/admin-api/guides/payment-methods)
Guides for creating [Admin API](/docs/admin-api/guides/external-checkout) orders with each supported payment method. The matrix below summarizes each method's flow type and capabilities — see the individual guide for request details and payment-specific data.
## Capability matrix [#capability-matrix]
| Payment Method | Flow | Express | Upsells | Subscriptions |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ------- | ------- | ------------- |
| [`card_token`](/docs/admin-api/guides/payment-methods/bankcard) | Direct & Redirect ([3DS](/docs/admin-api/guides/payment-methods/bankcard#3d-secure-3ds2)) | Yes | Yes | Yes |
| [`apple_pay`](/docs/admin-api/guides/payment-methods/apple-pay) | Redirect | Yes | Yes | Yes |
| [`google_pay`](/docs/admin-api/guides/payment-methods/google-pay) | Redirect | Yes | Yes | Yes |
| [`paypal`](/docs/admin-api/guides/payment-methods/paypal) | Redirect | Yes | Yes | Yes |
| [`klarna`](/docs/admin-api/guides/payment-methods/klarna) | Redirect | No | Yes | Yes |
| [`link`](/docs/admin-api/guides/payment-methods/link) | Redirect | Yes | Yes | Yes |
| [`twint`](/docs/admin-api/guides/payment-methods/twint) | Redirect | No | Yes | Yes |
| [`swish`](/docs/admin-api/guides/payment-methods/swish) | Redirect | No | No | No |
| [`affirm`](/docs/admin-api/guides/payment-methods/affirm) | Redirect | No | No | No |
| [`afterpay`](/docs/admin-api/guides/payment-methods/afterpay) | Redirect | No | No | No |
| [`bancontact`](/docs/admin-api/guides/payment-methods/bancontact) | Redirect | No | No | No |
| [`ideal`](/docs/admin-api/guides/payment-methods/ideal) | Redirect | No | No | No |
| [`sepa_direct`](/docs/admin-api/guides/payment-methods/sepa-debit) | Redirect | No | No | No |
**Upsells** (post-purchase, one-click) are added via [`ordersAddLineItemsCreate`](/docs/admin-api/reference/orders/ordersAddLineItemsCreate), which reuses the order's initial payment method to collect payment. This requires the payment method to support merchant-initiated charges — see each guide's **Upsells** section.
## Redirect payment flow [#redirect-payment-flow]
Every method except direct card tokenization completes payment through a redirect flow:
# Klarna Admin API Guide (/docs/admin-api/guides/payment-methods/klarna)
**Klarna** is a fully integrated payment method via NEXT Payments and Stripe, supported both in the storefront checkout and via the Admin API.
Klarna transactions send the customer through a Klarna redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get Klarna set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Klarna using the orders\_create API method, you must specify the `payment_method=klarna` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Klarna"
{
"payment_method": "klarna",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a Klarna account connected to a specific gateway.
### Redirect Customer to Klarna [#redirect-customer-to-klarna]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on Klarna.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Klarna supports one-click upsells through the [ordersAddLineItemsCreate](/docs/admin-api/reference/orders/ordersAddLineItemsCreate) API, enabling additional items to be added to the order with a payment transaction.
### Recurring [#recurring]
Klarna via NEXT Payments supports recurring transactions and can be used as a payment method for an order with subscription items.
# Link Admin API Guide (/docs/admin-api/guides/payment-methods/link)
**Link** is a fully integrated express payment method via Stripe, supported both in the storefront checkout and via the Admin API.
Link transactions send the customer through a Link redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get Link set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Link using the orders\_create API method, you must specify the `payment_method=link` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Link"
{
"payment_method": "link",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a Link account connected to a specific gateway.
### Redirect Customer to Link [#redirect-customer-to-link]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on Link.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Link supports one-click upsells through the [ordersAddLineItemsCreate](/docs/admin-api/reference/orders/ordersAddLineItemsCreate) API, enabling additional items to be added to the order with a payment transaction.
### Recurring [#recurring]
Link via Stripe supports recurring transactions and can be used as a payment method for an order with subscription items.
# PayPal Admin API Guide (/docs/admin-api/guides/payment-methods/paypal)
**PayPal** is a fully integrated payment app that is supported both in the storefront checkout, and via the Admin API. PayPal transactions send the customer through a PayPal redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get PayPal set up and working on the Admin API.
For custom PayPal checkouts, there are two checkout flows available, the standard method where a user enters their shipping address, chooses products, and then checks out via PayPal; and the "One-Click" method, where the user is not required to enter shipping information before being redirected to PayPal checkout.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using PayPal using the orders\_create API method, you must specify the `payment_method=paypal` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with PayPal"
{
"payment_method": "paypal",
"payment_details": {
"payment_return_url": "",
"paypal_account": "" // optional
}
}
```
You can optionally provide a `paypal_account` when creating the order to use a PayPal account other than the store default PayPal account.
### Redirect Customer to Paypal [#redirect-customer-to-paypal]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on PayPal.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": "https://www.paypal.com/checkoutnow?token="
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Paypal supports one-click upsells through the [ordersAddLineItemsCreate](/docs/admin-api/reference/orders/ordersAddLineItemsCreate) API, enabling additional items to be added to the order with a payment transaction.
To process upsells, the Paypal account must have [Reference Transactions](https://developer.paypal.com/api/nvp-soap/do-reference-transaction-soap/) enabled and configured on the store.
If the store Paypal account has reference transactions enabled, the [ordersCreate](/docs/admin-api/reference/orders/ordersCreate) API response will include `supports_post_purchase_upsells: true`, signaling you can process one-click upsell transactions.
# SEPA Direct Debit Admin API Guide (/docs/admin-api/guides/payment-methods/sepa-debit)
**SEPA Direct Debit** is a fully integrated payment method that is supported both in the storefront checkout, and via the Admin API.
SEPA Direct Debit transactions send the customer through a SEPA Direct Debit redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get SEPA Direct Debit set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using SEPA Direct Debit using the orders\_create API method, you must specify the `payment_method=sepa_debit` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with SEPA Direct Debit"
{
"payment_method": "sepa_debit",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a SEPA Direct Debit account connected to a specific gateway.
### Redirect Customer to SEPA Direct Debit [#redirect-customer-to-sepa-direct-debit]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on SEPA Direct Debit.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Upsells are not supported with SEPA Direct Debit payments.
# Swish Admin API Guide (/docs/admin-api/guides/payment-methods/swish)
**Swish** is a fully integrated payment method through NEXT Payments, supported in the storefront checkout and through the Admin API.
Swish transactions send the customer through a hosted payment flow, with the resulting order information returned to your application. Before using Swish, make sure it is enabled for the NEXT Payments account and store. Swish is available for eligible Swedish checkout traffic in SEK.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating an order with the `orders_create` API method, specify `payment_method=swish` and provide a `payment_return_url`. The `payment_return_url` is your endpoint that receives a POST request containing the final order data.
```json title="Payment Details for Order with Swish"
{
"payment_method": "swish",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` or `payment_gateway_group` to route the order through a specific eligible NEXT Payments configuration.
### Redirect Customer to Swish [#redirect-customer-to-swish]
The order response provides a `payment_complete_url`. Redirect the customer to this URL to complete the payment with Swish.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
One-click post-purchase upsells are not supported with Swish payments.
### Recurring [#recurring]
Swish cannot be used as the payment method for subscription items.
# Twint Admin API Guide (/docs/admin-api/guides/payment-methods/twint)
**Twint** is a fully integrated payment method via NEXT Payments supported both in the storefront checkout, and via the Admin API.
Twint transactions send the customer through a Twint redirect flow, with the resulting order information provided back to your application. Below are the steps needed to get Twint set up and working on the Admin API.
### API Payment Redirect Flow [#api-payment-redirect-flow]
### Create Order on Admin API [#create-order-on-admin-api]
When creating a new order using Twint using the orders\_create API method, you must specify the `payment_method=twint` as well as provide a `payment_return_url`. The `payment_return_url` is your endpoint that will receive a POST request containing the final order data.
```json title="Payment Details for Order with Twint"
{
"payment_method": "twint",
"payment_details": {
"payment_return_url": "",
"payment_gateway": "", // optional
"payment_gateway_group": "" // optional
}
}
```
You can optionally provide a `payment_gateway` when creating the order to use a Twint account connected to a specific gateway.
### Redirect Customer to Twint [#redirect-customer-to-twint]
The response when creating the order will provide a `payment_complete_url`. Your application should redirect the customer to this URL for completing the payment on Twint.
```json title="Response with Payment Complete URL"
{
"reference_transaction_id": null,
"payment_complete_url": ""
}
```
### Receiving Order Data [#receiving-order-data]
### Upsells [#upsells]
Twint supports one-click upsells through the [ordersAddLineItemsCreate](/docs/admin-api/reference/orders/ordersAddLineItemsCreate) API, enabling additional items to be added to the order with a payment transaction.
### Recurring [#recurring]
Twint via NEXT Payments supports recurring transactions and can be used as a payment method for an order with subscription items.
# Carts Create (/docs/admin-api/reference/carts/cartsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts Destroy (/docs/admin-api/reference/carts/cartsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts List (/docs/admin-api/reference/carts/cartsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts Retrieve (/docs/admin-api/reference/carts/cartsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts Update (/docs/admin-api/reference/carts/cartsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Create (/docs/admin-api/reference/campaigns/campaignsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Destroy (/docs/admin-api/reference/campaigns/campaignsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns List (/docs/admin-api/reference/campaigns/campaignsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Offers Create (/docs/admin-api/reference/campaigns/campaignsOffersCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Offers Destroy (/docs/admin-api/reference/campaigns/campaignsOffersDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Offers List (/docs/admin-api/reference/campaigns/campaignsOffersList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Offers Partial Update (/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Offers Retrieve (/docs/admin-api/reference/campaigns/campaignsOffersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Create (/docs/admin-api/reference/campaigns/campaignsPackagesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Destroy (/docs/admin-api/reference/campaigns/campaignsPackagesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Image Destroy (/docs/admin-api/reference/campaigns/campaignsPackagesImageDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Image Update (/docs/admin-api/reference/campaigns/campaignsPackagesImageUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages List (/docs/admin-api/reference/campaigns/campaignsPackagesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Partial Update (/docs/admin-api/reference/campaigns/campaignsPackagesPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Packages Retrieve (/docs/admin-api/reference/campaigns/campaignsPackagesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Partial Update (/docs/admin-api/reference/campaigns/campaignsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Retrieve (/docs/admin-api/reference/campaigns/campaignsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Shipping Methods Create (/docs/admin-api/reference/campaigns/campaignsShippingMethodsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Shipping Methods Destroy (/docs/admin-api/reference/campaigns/campaignsShippingMethodsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Shipping Methods List (/docs/admin-api/reference/campaigns/campaignsShippingMethodsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Shipping Methods Partial Update (/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaigns Shipping Methods Retrieve (/docs/admin-api/reference/campaigns/campaignsShippingMethodsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apps Retrieve (/docs/admin-api/reference/apps/appsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apps Settings Destroy (/docs/admin-api/reference/apps/appsSettingsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apps Settings Partial Update (/docs/admin-api/reference/apps/appsSettingsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apps Settings Retrieve (/docs/admin-api/reference/apps/appsSettingsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apps Settings Update (/docs/admin-api/reference/apps/appsSettingsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Addresses Create (/docs/admin-api/reference/customers/usersAddressesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Addresses Destroy (/docs/admin-api/reference/customers/usersAddressesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Addresses List (/docs/admin-api/reference/customers/usersAddressesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Addresses Retrieve (/docs/admin-api/reference/customers/usersAddressesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Addresses Update (/docs/admin-api/reference/customers/usersAddressesUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Create (/docs/admin-api/reference/customers/usersCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users List (/docs/admin-api/reference/customers/usersList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Notes Create (/docs/admin-api/reference/customers/usersNotesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Notes List (/docs/admin-api/reference/customers/usersNotesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Orders Retrieve (/docs/admin-api/reference/customers/usersOrdersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Partial Update (/docs/admin-api/reference/customers/usersPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Retrieve (/docs/admin-api/reference/customers/usersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Subscriptions Retrieve (/docs/admin-api/reference/customers/usersSubscriptionsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Users Update (/docs/admin-api/reference/customers/usersUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gift Cards Create (/docs/admin-api/reference/gift-cards/giftCardsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gift Cards Deactivate (/docs/admin-api/reference/gift-cards/giftCardsDeactivate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gift Cards List (/docs/admin-api/reference/gift-cards/giftCardsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gift Cards Partial Update (/docs/admin-api/reference/gift-cards/giftCardsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gift Cards Retrieve (/docs/admin-api/reference/gift-cards/giftCardsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata Create (/docs/admin-api/reference/metadata/metadataCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata Destroy (/docs/admin-api/reference/metadata/metadataDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata List (/docs/admin-api/reference/metadata/metadataList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata Partial Update (/docs/admin-api/reference/metadata/metadataPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata Retrieve (/docs/admin-api/reference/metadata/metadataRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Metadata Update (/docs/admin-api/reference/metadata/metadataUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exports Create (/docs/admin-api/reference/exports/exportsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exports Download Retrieve (/docs/admin-api/reference/exports/exportsDownloadRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exports List (/docs/admin-api/reference/exports/exportsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exports Retrieve (/docs/admin-api/reference/exports/exportsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exports Types Retrieve (/docs/admin-api/reference/exports/exportsTypesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Add Line Items Create (/docs/admin-api/reference/orders/ordersAddLineItemsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Cancel Create (/docs/admin-api/reference/orders/ordersCancelCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Capture Create (/docs/admin-api/reference/orders/ordersCaptureCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Collect Payment Create (/docs/admin-api/reference/orders/ordersCollectPaymentCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Create (/docs/admin-api/reference/orders/ordersCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillment Orders Retrieve (/docs/admin-api/reference/orders/ordersFulfillmentOrdersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillment Retrieve (/docs/admin-api/reference/orders/ordersFulfillmentRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Create (/docs/admin-api/reference/orders/ordersFulfillmentsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Events Create (/docs/admin-api/reference/orders/ordersFulfillmentsEventsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Events Destroy (/docs/admin-api/reference/orders/ordersFulfillmentsEventsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Events List (/docs/admin-api/reference/orders/ordersFulfillmentsEventsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Events Retrieve (/docs/admin-api/reference/orders/ordersFulfillmentsEventsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Fulfillments Retrieve (/docs/admin-api/reference/orders/ordersFulfillmentsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Lines Create (/docs/admin-api/reference/orders/ordersLinesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Lines Destroy (/docs/admin-api/reference/orders/ordersLinesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Lines Partial Update (/docs/admin-api/reference/orders/ordersLinesPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders List (/docs/admin-api/reference/orders/ordersList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Mark As Paid Create (/docs/admin-api/reference/orders/ordersMarkAsPaidCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Notes Create (/docs/admin-api/reference/orders/ordersNotesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Notes List (/docs/admin-api/reference/orders/ordersNotesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Partial Update (/docs/admin-api/reference/orders/ordersPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Refund Calculate Create (/docs/admin-api/reference/orders/ordersRefundCalculateCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Refund Create (/docs/admin-api/reference/orders/ordersRefundCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Retrieve (/docs/admin-api/reference/orders/ordersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Update (/docs/admin-api/reference/orders/ordersUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Store Detail (/docs/admin-api/reference/store/storeDetail)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Create (/docs/admin-api/reference/payments/disputesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Destroy (/docs/admin-api/reference/payments/disputesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes List (/docs/admin-api/reference/payments/disputesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Notes Create (/docs/admin-api/reference/payments/disputesNotesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Notes List (/docs/admin-api/reference/payments/disputesNotesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Partial Update (/docs/admin-api/reference/payments/disputesPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Resolve Create (/docs/admin-api/reference/payments/disputesResolveCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Retrieve (/docs/admin-api/reference/payments/disputesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Disputes Update (/docs/admin-api/reference/payments/disputesUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gateway Groups List (/docs/admin-api/reference/payments/gatewayGroupsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gateway Groups Retrieve (/docs/admin-api/reference/payments/gatewayGroupsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gateways List (/docs/admin-api/reference/payments/gatewaysList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Gateways Retrieve (/docs/admin-api/reference/payments/gatewaysRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Transactions Capture Create (/docs/admin-api/reference/payments/transactionsCaptureCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Transactions List (/docs/admin-api/reference/payments/transactionsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Transactions Refund Create (/docs/admin-api/reference/payments/transactionsRefundCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Transactions Retrieve (/docs/admin-api/reference/payments/transactionsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Transactions Verify Create (/docs/admin-api/reference/payments/transactionsVerifyCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Categories Create (/docs/admin-api/reference/products/categoriesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Categories List (/docs/admin-api/reference/products/categoriesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Categories Retrieve (/docs/admin-api/reference/products/categoriesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Categories Update (/docs/admin-api/reference/products/categoriesUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Create (/docs/admin-api/reference/products/productsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Destroy (/docs/admin-api/reference/products/productsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Image Create (/docs/admin-api/reference/products/productsImageCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Image Delete (/docs/admin-api/reference/products/productsImageDelete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Image Detail (/docs/admin-api/reference/products/productsImageDetail)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Image List (/docs/admin-api/reference/products/productsImageList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Image Update (/docs/admin-api/reference/products/productsImageUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products List (/docs/admin-api/reference/products/productsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Partial Update (/docs/admin-api/reference/products/productsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Prices Create (/docs/admin-api/reference/products/productsPricesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Prices Destroy (/docs/admin-api/reference/products/productsPricesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Prices List (/docs/admin-api/reference/products/productsPricesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Prices Partial Update (/docs/admin-api/reference/products/productsPricesPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Prices Retrieve (/docs/admin-api/reference/products/productsPricesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Retrieve (/docs/admin-api/reference/products/productsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Update (/docs/admin-api/reference/products/productsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variant Create (/docs/admin-api/reference/products/productsVariantCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variant List (/docs/admin-api/reference/products/productsVariantList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variants Destroy (/docs/admin-api/reference/products/productsVariantsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variants Partial Update (/docs/admin-api/reference/products/productsVariantsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variants Retrieve (/docs/admin-api/reference/products/productsVariantsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Products Variants Update (/docs/admin-api/reference/products/productsVariantsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Stockrecords Create (/docs/admin-api/reference/products/stockrecordsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Stockrecords Destroy (/docs/admin-api/reference/products/stockrecordsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Stockrecords List (/docs/admin-api/reference/products/stockrecordsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Stockrecords Partial Update (/docs/admin-api/reference/products/stockrecordsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Stockrecords Retrieve (/docs/admin-api/reference/products/stockrecordsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages Create (/docs/admin-api/reference/storefront/pagesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages Destroy (/docs/admin-api/reference/storefront/pagesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages List (/docs/admin-api/reference/storefront/pagesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages Partial Update (/docs/admin-api/reference/storefront/pagesPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages Retrieve (/docs/admin-api/reference/storefront/pagesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pages Update (/docs/admin-api/reference/storefront/pagesUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Create (/docs/admin-api/reference/storefront/themesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Destroy (/docs/admin-api/reference/storefront/themesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes List (/docs/admin-api/reference/storefront/themesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Retrieve (/docs/admin-api/reference/storefront/themesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Templates Create (/docs/admin-api/reference/storefront/themesTemplatesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Templates Destroy (/docs/admin-api/reference/storefront/themesTemplatesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Themes Templates Retrieve (/docs/admin-api/reference/storefront/themesTemplatesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Cancel Create (/docs/admin-api/reference/subscriptions/subscriptionsCancelCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Create (/docs/admin-api/reference/subscriptions/subscriptionsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Lines Create (/docs/admin-api/reference/subscriptions/subscriptionsLinesCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Lines Destroy (/docs/admin-api/reference/subscriptions/subscriptionsLinesDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Lines Update (/docs/admin-api/reference/subscriptions/subscriptionsLinesUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions List (/docs/admin-api/reference/subscriptions/subscriptionsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Partial Update (/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Pause Create (/docs/admin-api/reference/subscriptions/subscriptionsPauseCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Renew Create (/docs/admin-api/reference/subscriptions/subscriptionsRenewCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Resume Create (/docs/admin-api/reference/subscriptions/subscriptionsResumeCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Retrieve (/docs/admin-api/reference/subscriptions/subscriptionsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Retry Create (/docs/admin-api/reference/subscriptions/subscriptionsRetryCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Subscriptions Transactions List (/docs/admin-api/reference/subscriptions/subscriptionsTransactionsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Attachments Create (/docs/admin-api/reference/support/ticketsAttachmentsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Comments Create (/docs/admin-api/reference/support/ticketsCommentsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Comments Retrieve (/docs/admin-api/reference/support/ticketsCommentsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Create (/docs/admin-api/reference/support/ticketsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets List (/docs/admin-api/reference/support/ticketsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Partial Update (/docs/admin-api/reference/support/ticketsPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Retrieve (/docs/admin-api/reference/support/ticketsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickets Update (/docs/admin-api/reference/support/ticketsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickettypes List (/docs/admin-api/reference/support/tickettypesList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tickettypes Retrieve (/docs/admin-api/reference/support/tickettypesRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks Create (/docs/admin-api/reference/webhooks/webhooksCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks Destroy (/docs/admin-api/reference/webhooks/webhooksDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks List (/docs/admin-api/reference/webhooks/webhooksList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks Partial Update (/docs/admin-api/reference/webhooks/webhooksPartialUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks Retrieve (/docs/admin-api/reference/webhooks/webhooksRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhooks Update (/docs/admin-api/reference/webhooks/webhooksUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Assigned Fulfillment Orders List (/docs/admin-api/reference/fulfillment/assignedFulfillmentOrdersList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Available Locations Retrieve (/docs/admin-api/reference/fulfillment/availableLocationsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancellation Request Accept (/docs/admin-api/reference/fulfillment/cancellationRequestAccept)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancellation Request Cancel (/docs/admin-api/reference/fulfillment/cancellationRequestCancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancellation Request Reject (/docs/admin-api/reference/fulfillment/cancellationRequestReject)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancellation Request Send (/docs/admin-api/reference/fulfillment/cancellationRequestSend)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Cancel (/docs/admin-api/reference/fulfillment/fulfillmentOrdersCancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Close (/docs/admin-api/reference/fulfillment/fulfillmentOrdersClose)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Hold (/docs/admin-api/reference/fulfillment/fulfillmentOrdersHold)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders List (/docs/admin-api/reference/fulfillment/fulfillmentOrdersList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Move (/docs/admin-api/reference/fulfillment/fulfillmentOrdersMove)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Release Hold (/docs/admin-api/reference/fulfillment/fulfillmentOrdersReleaseHold)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Orders Retrieve (/docs/admin-api/reference/fulfillment/fulfillmentOrdersRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Request Accept (/docs/admin-api/reference/fulfillment/fulfillmentRequestAccept)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Request Cancel (/docs/admin-api/reference/fulfillment/fulfillmentRequestCancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Request Reject (/docs/admin-api/reference/fulfillment/fulfillmentRequestReject)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillment Request Send (/docs/admin-api/reference/fulfillment/fulfillmentRequestSend)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillments Create (/docs/admin-api/reference/fulfillment/fulfillmentsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fulfillments Retrieve (/docs/admin-api/reference/fulfillment/fulfillmentsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Locations Create (/docs/admin-api/reference/fulfillment/locationsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Locations Destroy (/docs/admin-api/reference/fulfillment/locationsDestroy)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Locations List (/docs/admin-api/reference/fulfillment/locationsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Locations Retrieve (/docs/admin-api/reference/fulfillment/locationsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Locations Update (/docs/admin-api/reference/fulfillment/locationsUpdate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Shipping Methods List (/docs/admin-api/reference/fulfillment/shippingMethodsList)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Shipping Methods Retrieve (/docs/admin-api/reference/fulfillment/shippingMethodsRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Address Autocomplete (/docs/campaigns/api/address/addressAutocomplete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts Calculate (/docs/campaigns/api/carts/cartsCalculate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Carts Create (/docs/campaigns/api/carts/cartsCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Campaign Retrieve (/docs/campaigns/api/campaigns/campaignRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Order Retrieve (/docs/campaigns/api/orders/orderRetrieve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Create (/docs/campaigns/api/orders/ordersCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Orders Upsell Create (/docs/campaigns/api/orders/ordersUpsellCreate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# addCartLines (/docs/storefront/graphql/mutations/add-cart-lines)
# addVoucher (/docs/storefront/graphql/mutations/add-voucher)
# createCart (/docs/storefront/graphql/mutations/create-cart)
# emptyCart (/docs/storefront/graphql/mutations/empty-cart)
# register (/docs/storefront/graphql/mutations/register)
# removeCartLines (/docs/storefront/graphql/mutations/remove-cart-lines)
# removeVoucher (/docs/storefront/graphql/mutations/remove-voucher)
# tokenAuth (/docs/storefront/graphql/mutations/token-auth)
# updateAccount (/docs/storefront/graphql/mutations/update-account)
# updateCartAttribution (/docs/storefront/graphql/mutations/update-cart-attribution)
# updateCartLines (/docs/storefront/graphql/mutations/update-cart-lines)
# updateCartMetadata (/docs/storefront/graphql/mutations/update-cart-metadata)
# verifyToken (/docs/storefront/graphql/mutations/verify-token)
# cart (/docs/storefront/graphql/queries/cart)
# me (/docs/storefront/graphql/queries/me)
# product (/docs/storefront/graphql/queries/product)
# products (/docs/storefront/graphql/queries/products)
# Custom Page Templates (/docs/storefront/themes/guides/custom-page-templates)
Pages created in the storefront dashboard (**Storefront > Pages**) can have very diverse design requirements that often require custom layouts. In this guide, we'll go over some of the best practices for creating and managing custom page templates.
### Page Templates Location [#page-templates-location]
In the `templates/pages` directory of a theme, theme developers can edit/manage the custom page templates.
```bash title="Page Templates Location"
pages
└── page.html (default)
└── page..html
└── page..html
```
### Extend & Override [#extend--override]
**Create a Custom Page Template**
Create a new page template in the `templates/pages` directory with the following naming convention:
```bash title="Custom Page Template Naming"
templates/pages/page..html
```
Templates that follow this naming convention will be selectable from the page detail area in the storefront dashboard.
**Extend & Override Default Page Template**
As a best practice, you should [extend](/docs/storefront/themes/templates/tags#extends--block) the default page template to override the necessary [template blocks](/docs/storefront/themes/templates/tags#extends--block) to achieve your customization with a limited amount of duplicate code.
```jinja title="Example Custom Page Template"
{% extends "templates/pages/page.html" %}
{% block content %}
// Custom Page Content Template Code
{% endblock %}
```
This strategy will simplify the creation and management of custom page templates so you can focus on the customized areas for the new custom product template.
**Select Template for Page**
On your page of choice, select your newly created template as the **Theme Template** to activate the template for your page in the storefront.
After uploading and selecting the template, navigate to that page's full storefront URL on the `https://{store}.29next.store` network domain and verify the served HTML, expected assets, and behavior. Do not use a mapped public storefront domain to decide whether the template change landed.
You are not limited to overriding the existing template blocks in the default page template. You can create and add your own to the default template to overide in your custom template. For example, adding `{% block my_custom_block %}{% endblock %}` to page.html, around any area you wish to customize, will allow you to overide it in the custom template
# Custom Product Templates (/docs/storefront/themes/guides/custom-product-templates)
Products can have very diverse design requirements that often require custom layouts. In this guide, we'll go over some of the best practices for creating and managing custom product templates.
### Product Templates Location [#product-templates-location]
In the `templates/catalogue` directory of a theme, theme developers can edit/manage the product page templates.
```bash title="Product Templates Location"
catalogue
└── product.html (default)
└── product..html
└── product..html
```
### Extend & Override [#extend--override]
**Create a Custom Product Template**
Create a new product template in the **templates>catalogue** directory with the following naming convention:
```bash title="Custom Product Template Naming"
templates/catalogue/product..html
```
Templates that follow this naming convention will be selectable on the product detail to use on the storefront.
**Extend & Override Default Product Template**
As a best practice, you should [extend](/docs/storefront/themes/templates/tags#extends--block) the default product template to override the necessary [template blocks](/docs/storefront/themes/templates/tags#extends--block) to achieve your customization with a limited amount of duplicate code.
```jinja title="Example Custom Product Template"
{% extends "templates/catalogue/product.html" %}
{% block header %}
// Custom Product Header Code
{% endblock header %}
{% block product_description %}
// Custom Product Content Template Code
{% endblock %}
```
This strategy will simplify the creation and management of custom product templates so you can focus on the customized areas for the new custom product template.
**Select Template for Product**
On your product of choice, select your newly created template as the Product Template to activate the template on your product in the storefront.
After uploading and selecting the template, navigate to that product's full storefront URL on the `https://{store}.29next.store` network domain and verify the served HTML, expected assets, and behavior. Do not use a mapped public storefront domain to decide whether the template change landed.
You are not limited to overriding the existing template blocks in the default product template. You can create and add your own to the default template to overide in your custom template. For example, adding `{% block my_custom_block %}{% endblock %}` to product.html, around any area you wish to customize, will allow you to overide it in the custom template
# Personalized Products Guide (/docs/storefront/themes/guides/personalized-products)
Some products need customer input at the time of purchase — an engraving on a mug, a monogram on a bag, a gift message on a card. This information is captured as **line item properties**: name and value pairs attached to a cart line rather than to the product itself. No variant, SKU, or inventory is required for each possible value.
### Add Property Inputs to the Product Template [#add-property-inputs-to-the-product-template]
Property inputs are named `properties[]`, where `` is the label stored with the line. Add them inside the [add-to-cart form](/docs/storefront/themes/templates/tags#cart_form) alongside the fields generated by the `cart_form` tag.
The engraving label and input are the only addition to the add-to-cart form.
```jinja title="templates/catalogue/product.html"
{% purchase_info_for_product request product as session %}
{% if session.availability.is_available_to_buy %}
{% cart_form request product 'single' as cart_form %}
{% else %}
{% t "store.catalogue.out_of_stock" %}
{% endif %}
```
Add one input per property. A mug with an engraving and a font choice uses `properties[Engraving]` and `properties[Font]`.
### Display Properties in the Cart [#display-properties-in-the-cart]
The cart template receives a `formset` of cart line forms. Each `form.instance` is a [line](/docs/storefront/themes/templates/objects#line) with a `properties` list of `key` and `value` pairs.
```jinja title="partials/cart_line_properties.html"
{% for property in properties %}
{% if property.value %}
{{ property.key }}: {{ property.value }}
{% endif %}
{% endfor %}
```
```jinja title="templates/cart.html"
{% for form in formset %}
{% with line=form.instance %}
{% include "partials/cart_line_properties.html" with properties=line.properties %}
{% endwith %}
{% endfor %}
```
Themes that ship their own side cart JavaScript need to render properties there too. Request `properties { key value }` on the cart lines in your side cart query and output them alongside the product title.
### Behavior to Expect [#behavior-to-expect]
| Behavior | Detail |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unique lines | The same product added with different property values creates a separate cart line for each combination. Adding it again with identical values increases the quantity of the existing line. |
| Empty values | A property submitted with an empty value does not create a separate line. It is still recorded on the line, so guard your display with `{% if property.value %}`. |
| Hidden properties | Property names starting with an underscore, such as `properties[_source]`, are stored but excluded from `line.properties` and from the Storefront GraphQL API. Use them for data that should not be shown to customers. |
| Value length | Values longer than 500 characters are truncated to 500 characters. |
| Checkboxes | When a checkbox named `properties[...]` changes, the value submitted is `yes` or `no`. |
| File uploads | File inputs are not supported. Inputs with `type="file"` are ignored. |
| Set once | Properties are captured when the line is added to the cart. There is no update path for changing them afterwards. |
### Add Properties with the Storefront GraphQL API [#add-properties-with-the-storefront-graphql-api]
Themes that add to the cart through the [Storefront GraphQL API](/docs/storefront/graphql) pass properties on each line as a JSON object of name and value pairs. The field is available on [createCart](/docs/storefront/graphql/mutations/create-cart) and [addCartLines](/docs/storefront/graphql/mutations/add-cart-lines), and is returned on cart lines as `properties { key value }`.
```json title="addCartLines Variables"
{
"input": {
"cartId": "",
"lines": [
{
"productPk": 1,
"quantity": 1,
"properties": { "Engraving": "Alex" }
}
]
}
}
```
`properties` must be a JSON object. Any other value returns the error `Properties must be a JSON object.`
### Related [#related]
# Product Metadata (/docs/storefront/themes/guides/product-metadata)
Product metadata lets you add custom data for products to use in theme templates. This provides a robust structured way for theme developers to customize products display in the storefront. [See our user guide on adding custom metadata fields](https://docs.nextcommerce.com/docs/build-a-store/technical-settings/metadata-fields-and-tags).
### Template Access [#template-access]
Product metadata values are accessible in theme templates through their metadata `key`.
```jinja title="Template Access"
{{ product.metadata. }}
```
Values set for the given the product metadata will render in the product template.
### Example [#example]
Let's look at an example of adding support for a **Product Tagline** to a product template that can be set on an individual product basis in the dashboard.
**Create Tagline Product Metadata Field**
In your store Metadata settings, create a new Metadata Definition for your tagline. Set the the **object** to `Product` and **key** to `tagline`.
**Add Tagline Attribute Variable to Product Template**
In your theme's product template, add the code below to render the tagline by accessing it with a template variable.
```jinja title="Example"
{{ product.metadata.tagline }}
```
**Add Tagline Value to Product**
In your product metadata settings, add your tagline field with a value to render the your storefront product details. :clap:
# Product Variants Guide (/docs/storefront/themes/guides/product-variants)
Products with multiple variants are very common, for example, a shirt with 3 colors (Blue, Green, Red) and 4 sizes (S,M,L,XL) would have a total of 12 actual product choices (SKUs). Presenting the variant choices to users can add significant complexity for theme developers to create great user experiences for customers within the catalogue. Let's go over how Products with variants and their attributes can be mapped together in the storefront product details template.
### Variant Attribute Choices [#variant-attribute-choices]
The first step to adding variant support is adding the Variant Attribute Selectors in your product template to allow a user to see the variant attribute choices.Looping over the `variant_form` template object provides a path to dynamically creating the variant attribute choice selectors driven by the product configuration.
```jinja title="templates/catalogue/product.html"
{% for field in variant_form %}
{% if 'attr' in field.id_for_label %}
{% include "partials/form_field.html" with field=field %}
{% endif %}
{% endfor %}
```
Using the choice fields from the template is entirely optional. Theme developers can create their own custom choice selectors using the `product.data` json object for a more customized user experience.
### Map Choices to Variants [#map-choices-to-variants]
With the variant choices now available in the template, it is now necessary to map choices from the `variant_form` choices to variant product IDs.
*Accessing all variant product data*
Use the `product.data` object in your template to generate a detailed json object of the product images, variant attributes, variant prices, and variant availability.
```jinja title="Map Choices to Variants"
{{ product.data|json_script:"product-data" }}
```
Use javascript in your template to map the variant select fields to the available product ID and update the add-to-cart form to add the chosen variant. See full example in our Base Theme.
# Filter Reference (/docs/storefront/themes/templates/filters)
## Arrays & Lists [#arrays--lists]
### dictsort [#dictsort]
Takes a list of dictionaries and returns that list sorted by the key given in the argument.
```jinja title="dictsort"
{{ value|dictsort:"name" }}
```
For example:
```jinja title="dictsort"
{{ value|dictsort:"category" }}
```
If value is:
```json title="dictsort"
[
{'category': 'B', 'count': 4},
{'category': 'C', 'count': 22},
{'category': 'A', 'count': 12}
]
```
then the output would be:
```json title="dictsort Output"
[
{'category': 'A', 'count': 12},
{'category': 'B', 'count': 4},
{'category': 'B', 'count': 4}
]
```
### dictsortedreversed [#dictsortedreversed]
Takes a list of dictionaries and returns that list sorted in reverse order by the key given in the argument. This works exactly the same as the above filter, but the returned value will be in reverse order.
### first [#first]
Returns the first item in a list.
```jinja title="first"
{{ value|first }}
```
For example, if value is the list \['a', 'b', 'c'], the output will be 'a'.
### join [#join]
Joins a list with a string.
```jinja title="join"
{{ value|join:" - " }}
```
For example, if value is the list \['a', 'b', 'c'], the output will be the string "a - b - c".
### last [#last]
Returns the last item in a list.
```jinja title="last"
{{ value|last }}
```
For example, if value is the list \['a', 'b', 'c', 'd'], the output will be the string "d".
### slice [#slice]
Returns a slice of the list.
```jinja title="slice"
{{ some_list|slice:":2" }}
```
For example, if some\_list is \['a', 'b', 'c'], and slice to first 2, the output will be \['a', 'b'].
### unordered\_list [#unordered_list]
Recursively takes a self-nested list and returns an HTML unordered list – WITHOUT opening and closing `` tags. The list is assumed to be in the proper format.
```jinja title="unordered_list"
{{ value|unordered_list }}
```
For example, if var contains `['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]`, then `{{ var|unordered_list }}` would return:
```jinja title="unordered_list"
States
```
## Default [#default]
### default [#default-1]
If value evaluates to False, uses the given default. Otherwise, uses the value.
```jinja title="default"
{{ value|default:"something" }}
```
For example, If value is "" (an empty string), the output will be something.
### default\_if\_none [#default_if_none]
If (and only if) value is None, uses the given default. Otherwise, uses the value. Note that if an empty string is given, the default value will not be used. Use the default filter if you want to fallback for empty strings.
```jinja title="default_if_none"
{{ value|default_if_none:"something" }}
```
For example, if value is None, the output will be something.
## Format [#format]
### date [#date]
Formats a date according to the given format.
```jinja title="date"
{{ value|date:"D d M Y" }}
```
For example, `{{ value|date:"D d M Y" }}` this would convert a date to this format output will be the string Wed 09 Jan 2020. See available date reference for format options.
### escape [#escape]
Escapes a string’s HTML. Specifically, it makes these replacements:
\< is converted to \<> is converted to >' (single quote) is converted to '" (double quote) is converted to "& is converted to &
```jinja title="escape"
{{ title|escape }}
```
### escapejs [#escapejs]
Escapes characters for use in JavaScript strings. This does not make the string safe for use in HTML or JavaScript template literals, but does protect you from syntax errors when using templates to generate JavaScript/JSON.
```jinja title="escapejs"
{{ value|escapejs }}
```
For example, if value is escaping , the output will be \u003Cb\u003Eescaping\u003C/b\u003E
### json\_script [#json_script]
Safely outputs a variable object as JSON, wrapped in a `
```
### make\_list [#make_list]
Returns the value turned into a list. For a string, it’s a list of characters. For an integer, the argument is cast to a string before creating a list.
```jinja title="make_list"
{{ value|make_list }}
```
## Internationalization [#internationalization]
### language\_name\_local [#language_name_local]
Returns a localized name of the language.
```jinja title="language_name_local"
{{ LANGUAGE_CODE|language_name_local }}
```
For example, if the value is `fr`, the output would be `Français`.
## Integers [#integers]
### divisibleby [#divisibleby]
Returns True if the value is divisible by the argument.
```jinja title="divisibleby"
{{ value|divisibleby:"3" }}
```
For example, if value is 21, the output would be True.
### floatformat [#floatformat]
Allows you to specify the number of decimal places to format a float to.
```jinja title="floatformat"
{{ value|floatformat:2 }}
```
For example, if value is 34.2342 and use floatformat:2, the output will be 34.23.
## HTML [#html]
### linebreaks [#linebreaks]
Replaces line breaks in plain text with appropriate HTML; a single newline becomes an HTML line break ` ` and a new line followed by a blank line becomes a paragraph break `
`.
```jinja title="linebreaks"
{{ value|linebreaks }}
```
For example, if value is Joel\nis a slug, the output will be `Joel is a slug
`.
### linebreaksbr [#linebreaksbr]
Converts all newlines in a piece of plain text to HTML line breaks ` `. If value is Sandy is a slug, the output will be `Sandy is a slug`.
```jinja title="linebreaksbr"
{{ value|linebreaksbr }}
```
### truncatewords\_html [#truncatewords_html]
Similar to truncatechars, except that it is aware of HTML tags. Any tags that are opened in the string and not closed before the truncation point are closed immediately after the truncation.
```jinja title="truncatewords_html"
{{ value|truncatechars_html:7 }}
```
For example, if value is `Sandy is a slug
`, the output will be `Sandy i…
`.
## Strings [#strings]
### capfirst [#capfirst]
Capitalizes the first character of the value.
```jinja title="capfirst"
{{ value|capfirst }}
```
For example, if the first character is not a letter, this filter has no effect. For example, if value is chicago, the output will be Chicago.
### cut [#cut]
Removes all values of arg from the given string.
```jinja title="cut"
{{ value|cut:" " }}
```
For example, if value is String with spaces, the output will be Stringwithspaces.
### length [#length]
Returns the length of the value. This works for both strings and lists.
```jinja title="length"
{{ value|length }}
```
For example, if value is `['a', 'b', 'c', 'd']` or "abcd", the output will be 4.
### length\_is [#length_is]
Returns True if the value’s length is the argument, or False otherwise.
```jinja title="length_is"
{{ value|length_is:"4" }}
```
For example, if value is \['a', 'b', 'c', 'd'] or "abcd", the output will be True.
### linenumbers [#linenumbers]
Displays text with line numbers.
```jinja title="linenumbers"
{{ value|linenumbers }}
```
For example, if value is:
```jinja title="linenumbers"
one
two
three
```
the output will be:
```jinja title="linenumbers Output"
1. one
2. two
3. three
```
### lower [#lower]
Converts a string into all lowercase.
```jinja title="lower"
{{ value|lower }}
```
For example, if value is Totally LOVING this Product!, the output will be totally loving this product!.
For example, if value is the string "Sandy", the output would be the list \['S', 'a', 'n', 'd', 'y']. If value is 123, the output will be the list \['1', '2', '3'].
### pluralize [#pluralize]
Returns a plural suffix if the value is not 1, '1', or an object of length 1. By default, this suffix is 's'.
```jinja title="pluralize"
You have {{ num_messages }} message{{ num_messages|pluralize }}
```
### split [#split]
Splits a string by the given delimiter and returns a list.
```jinja title="split"
{{ value|split:"," }}
```
For example, if value is `"red,green,blue"`, the output will be the list `['red', 'green', 'blue']`.
### slugify [#slugify]
Converts to ASCII. Converts spaces to hyphens. Removes characters that aren’t alphanumerics, underscores, or hyphens. Converts to lowercase. Also strips
leading and trailing whitespace.
```jinja title="slugify"
{{ value|slugify }}
```
For example, if value is "Sandy is a slug", the output will be "sandy-is-a-slug".
### title [#title]
Converts a string into titlecase by making words start with an uppercase character and the remaining characters lowercase. This tag makes no effort to keep “trivial words” in lowercase.
```jinja title="title"
{{ value|title }}
```
For example, if value is "my FIRST post", the output will be "My First Post".
### truncatewords [#truncatewords]
Truncates a string after a certain number of words based on the argument.
```jinja title="truncatewords"
{{ value|truncatewords:2 }}
```
For example, if value is "Sandy is a slug", the output will be "Sandy is …".
### upper [#upper]
Converts a string into all uppercase.
```jinja title="upper"
{{ value|upper }}
```
For example, if value is "Sandy is a slug", the output will be "SANDY IS A SLUG".
### urlencode [#urlencode]
Escapes a value for use in a URL.
```jinja title="urlencode"
{{ value|urlencode }}
```
For example, if value is "[https://www.example.org/](https://www.example.org/)", the output will be "https%3A%2F%2Fwww\.example.org%2F".
### wordcount [#wordcount]
Returns the number of words.
```jinja title="wordcount"
{{ value|wordcount }}
```
For example, if value is "Joel is a slug", the output will be 4.
## Currency [#currency]
### currency [#currency-1]
Formats a decimal value as a currency string using the provided currency code. This is the primary filter for displaying prices throughout a theme.
```jinja title="currency"
{{ session.price.price|currency:session.price.currency }}
```
For example, if the price is `29.99` and the currency is `USD`, the output will be `$29.99`. The filter handles currency symbol placement and formatting based on the currency code.
```jinja title="Example Product Price Display"
{% purchase_info_for_product request product as session %}
{% if session.price.exists %}
{% if session.price.price_retail %}
{{ session.price.price_retail|currency:session.price.currency }}
{% endif %}
{{ session.price.price|currency:session.price.currency }}
{% endif %}
```
## Files [#files]
### asset\_url [#asset_url]
The asset\_url filter can be applied to theme asset files to generate CDN link to the asset for loading in the template HTML. The file argument is relative to the assets directory of the theme.
```jinja title="asset_url"
{{ 'style.css'|asset_url }}
```
## Math [#math]
### add [#add]
Adds the argument to the value.
```jinja title="add"
{{ value|add:2 }}
```
For example, if value is 4, then the output will be 6. The filter will try to force both values to integers. If this fails, it’ll attempt to add the values together anyway. If it fails, the result will be an empty string.
### abs [#abs]
Returns the absolute value of a number.
```jinja title="abs"
{{ value|abs }}
```
For example, if value is -3, the output would be 3. The filter will return the absolute value.
### atleast [#atleast]
Limits a number to a minimum value.
```jinja title="atleast"
{{ value|atleast:5 }}
```
For example, if the value is 3, the filter would return 5.
### atmost [#atmost]
Limits a number to a maximum value.
```jinja title="atmost"
{{ value|atmost:5 }}
```
For example, if the value is 7, the filter would return 5.
### ceil [#ceil]
Rounds a number up to the nearest integer.
```jinja title="ceil"
{{ value|ceil }}
```
For example, if the value was 1.2, the filter would return 2.
### dividedby [#dividedby]
Divides a number by a given number.
```jinja title="dividedby"
{{ value|dividedby:3 }}
```
For example, if the value was 9, the filter would return 3.
### floor [#floor]
Rounds a number down to the nearest integer.
```jinja title="floor"
{{ value|floor }}
```
For example, if the value was 1.2, the filter would return 1.
### minus [#minus]
Subtracts a given number from another number.
```jinja title="minus"
{{ value|minus:2.5 }}
```
For example, if the value was 5, the filter would return 2.5.
### modulo [#modulo]
Returns the remainder of dividing a number by a given number.
```jinja title="modulo"
{{ value|modulo:5 }}
```
For example, if the value was 12, the filter would return 2.
### plus [#plus]
Adds two numbers.
```jinja title="plus"
{{ value|plus:5 }}
```
For example, if the value was 5, the filter would return 10.
### round [#round]
Rounds a number to the nearest integer.
```jinja title="round"
{{ value|round }}
```
For example, if the value was 2.7, the filter would return 3.
### times [#times]
Multiplies a number by a given number.
```jinja title="times"
{{ value|times:3 }}
```
For example if the value was 2, the filter would return 9.
# Templates (/docs/storefront/themes/templates)
### Introduction [#introduction]
The storefront theme templates language is designed to be both powerful and easy to use. If you have any exposure to working with other text-based template languages such as Jinja2 or Liquid, you should feel right at home.
The storefront theme template system provides **tags, filters** and **variables** for control flow logic inside of a template.
### Variables [#variables]
Variables look like this: `{{ variable }}` and contain the content the template uses to render to the page. Variables contain a dictionary structure of content and use . notation to access attributes.
```jinja title="Variables"
Hello {{ customer.name }} !
```
```html title="Variables"
Hello John!
```
### Filters [#filters]
Filters allow you to modify the output of a variables and look like this `{{ customer.name|title }}`. This would display the value of `{{ customer.name }}` after being filtered there the title filter to make format the string to title case. See built-in filter reference.
```jinja title="Filters"
Hello {{ customer.name|title }}!
```
```html title="Result"
Hello John!
```
### Tags [#tags]
Tags can do many things such as control flow, iterations, template inheritance, and theme translations. Tags look like this `{% tag %}` . [See built-in tag reference](/docs/storefront/themes/templates/tags).
```jinja title="Tags"
{% if products %}
{% for product in products %}
{{ product.title }}
{% endfor %}
{% else %}
No products found.
{% endif %}
```
```html title="Result"
Product A
Product B
Product C
```
Using these core building blocks you can create fully customized shopping experiences for customers.
# Object Reference (/docs/storefront/themes/templates/objects)
Objects are template variables you can use to dynamically populate templates in your storefront theme. See documentation below and details of available template objects and their properties.
## Global Objects [#global-objects]
Global objects are available across all templates and pages enabling theme developers to create dynamic custom pages powered by the store data.
### currencies [#currencies]
Returns a list of active storefront currencies you can iterate over, see [currency](#currency).
```jinja title="Example Storefront Change Currency Form"
{% if currencies and currencies|length > 1 %}
{% endif %}
```
### languages\_active\_storefront [#languages_active_storefront]
Returns a list of active storefront languages you can iterate over, see [language](#language).
```jinja title="Example Storefront Change Language Form"
{% if languages_active_storefront %}
{% endif %}
```
### menus [#menus]
Allows you to access a menu's items by its code to iterate over to generate a menu from the backend, see [menu items](#items-menu). Menus are configured in the dashboard at **Storefront > Navigation**.
```jinja title="Example Dynamic Menu"
{% for item in menus.header_menu.items %}
{% if item.level > 0 %}
{{ item.name|title }}
{% else %}
{{ item.name|title }}
{% endif %}
{% endfor %}
```
Storefront Menus can be up to 3 levels, ensure your custom menu supports 2 nested menu item levels, see child and grandchild above.
### products [#products]
Returns a list of products you can iterate over, see [product](#product).
```jinja title="Example Storefront Products Query and Loop"
{% where products 'title' 'contains' 'featured' as products_filtered %}
{% for product in products_filtered %}
{% with image=product.primary_image %}
{% image_thumbnail image.original "350x350" crop="center" upscale=True as thumb %}
{% endwith %}
{% purchase_info_for_product request product as session %}
{% if session.price.exists %}
{{ session.price.price|currency:session.price.currency }}
{% else %}
{% endfor %}
```
### product\_categories [#product_categories]
Returns a list of product categories you can iterate over, see [product\_category](#product_category).
```jinja title="Example Storefront Product Categories Query and Loop"
{% where product_categories 'id' 'exact' 1 as homepage_categories %}
{% for category in homepage_categories %}
{% with image=category.image %}
{% if image %}
{% image_thumbnail image "350x350" crop="center" upscale=True as thumb %}
{% endif %}
{% endwith %}
{{ category.name }}
{{ category.description|safe }}
Shop now
{% endfor %}
```
### posts [#posts]
Returns a list of blog posts you can iterate over, see [post](#post).
```jinja title="Example Storefront Recent Blog Posts Query and Loop"
{% for post in posts|slice:"3" %}
{% with image=post.featured_image %}
{% if image %}
{% image_thumbnail image "400x250" upscale=False crop="top" as thumb %}
{% endif %}
{% endwith %}
{{post.posted_date|date:"M d, Y"}}
{{ post.content|striptags|truncatechars_html:140 }}
{% endfor %}
```
### post\_categories [#post_categories]
Returns a list of post categories you can iterate over, see [post\_category](#post_category).
```jinja title="Example Storefront Post Categories Query and Loop"
```
### privacy\_policy [#privacy_policy]
Content from store Privacy Policy settings, typically used in a "Privacy Policy" page to automatically pull content in from settings.
Store policies are configured in the dashboard at **Settings > Policies**. The content entered there is automatically available as global template variables.
```jinja title="privacy_policy"
{{ privacy_policy }}
```
### request [#request]
The current session active request context.
```jinja title="Example Request Object Usage"
```
| Property | Type | Description |
| --------------- | ------ | ------------------------------------------------- |
| `get_host` | String | Current host domain. |
| `path` | String | Current url path. |
| `COUNTRY_CODE` | String | Current active geo country code, see [geo](#geo). |
| `CURRENCY_CODE` | String | Current active currency code. |
| `LANGUAGE_CODE` | String | Current active language code. |
### settings [#settings]
Theme settings object with stored theme settings values as properties. See [theme settings](/docs/storefront/themes/settings) docs.
```jinja title="Example Colors Styles From Theme Settings"
```
### store [#store]
Returns the store object with general information about the store and the contact details.
```jinja title="Example Store Object Usage"
{{ store.legal_name }}
{{ store.address.line_1 }}
{% if store.address.line_2 %}{{ store.address.line_2 }} {% endif %}
{{ store.address.city }}, {{ store.address.state }} {{ store.address.postcode }}
{{ store.address.country }}
Email Support
```
| Property | Type | Description |
| ---------------------- | ------ | ----------------------------------------------------- |
| `address` | object | The store address object see [address](#address). |
| `branding` | object | The store branding object, see [branding](#branding). |
| `name` | String | General name of the store defined in settings. |
| `tagline` | String | Store tagline defined in settings. |
| `legal_name` | String | Legal name of the store. |
| `phone` | String | Store phone number. |
| `email` | String | Store email address. |
| `timezone` | String | Store timezone. |
| `schema` | String | Store schema, ie the store network subdomain. |
| `get_meta_title` | String | Store SEO meta title. |
| `get_meta_description` | String | Store SEO meta description. |
### storefront\_geos [#storefront_geos]
Returns a list of configured markets, see [geos](#geo).
```jinja title="Example Storefront Geo Switcher"
{% if storefront_geos and storefront_geos|length > 1 %}
Language/Currency
{% endif %}
```
### subscription\_terms\_and\_conditions [#subscription_terms_and_conditions]
Content from store subscription terms and conditions settings, typically used in a "Subscription Terms & Conditions" page to automatically pull content in from settings.
```jinja title="subscription_terms_and_conditions"
{{ subscription_terms_and_conditions }}
```
### terms\_and\_conditions [#terms_and_conditions]
Content from store terms and conditions settings, typically used in a "Terms & Conditions" page to automatically pull content in from settings.
```jinja title="terms_and_conditions"
{{ terms_and_conditions }}
```
## Objects [#objects]
Object have many properties that can be accessed in templates.
### address [#address]
| Property | Type | Description |
| ---------- | ------ | ----------------- |
| `line_1` | String | Address line 1. |
| `line_2` | String | Address line 2. |
| `city` | String | Address City. |
| `state` | String | Address State. |
| `postcode` | String | Address Postcode. |
| `country` | String | Address Country. |
### branding [#branding]
Store branding properties accessed through the [store](#store) object to leverage within templates. Branding values are configured in the dashboard at **Settings > Branding**.
| Property | Type | Description |
| --------------- | ------ | -------------------------------------------------------- |
| `logo` | File | Store branding logo, use `.url` to access the file link. |
| `icon` | File | Store branding icon, use `.url` to access the file link. |
| `primary_color` | String | Store branding primary color, returns a HEX code. |
| `accent_color` | String | Store branding accent color, returns a HEX code. |
### currency [#currency]
Currency object accessed through [currencies](#currencies).
| Property | Type | Description |
| -------- | ------ | ----------------------------------- |
| `code` | String | Name of the currency, ie `USD`. |
| `symbol` | String | The symbol of the currency, ie `$`. |
### country [#country]
Country object to access properties about a country, see [storefront\_geos](#storefront_geos) for example usage.
| Property | Type | Description |
| -------- | ------ | ------------------------------------------------------------------------------------------------- |
| `code` | String | Two letter [ISO 3166](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) country code. |
| `name` | String | Full name of the country. |
### geo [#geo]
A `geo` is a combination of a language and currency typically associated with a market, see full example in [storefront\_geos](#storefront_geos).
| Property | Type | Description |
| ---------- | ------ | ------------------------------------------------------------------------------------------------- |
| `currency` | object | The currency object, see [currency](#currency). |
| `country` | object | The country object, see [country](#country) |
| `language` | String | Two letter [ISO 639](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code. |
### image [#image]
Product images, see example usage below.
```jinja title="image"
{% with all_images=product.get_all_images %}
{% for image in all_images %}{% image_thumbnail image.original "100x100" crop="center" as thumb %}
{% endfor %}
{% endif %}
{% endwith %}
```
| Property | Type | Description |
| ---------- | ------ | ------------------------------------------------------------------ |
| `original` | File | The original image file, typically used when creating a thumbnail. |
| `url` | String | The full CDN link to render the image. |
### items (menu) [#items-menu]
A menu `item` has properties to support creating dynamic menus configured through the dashboard menu editor.
```jinja title="Example Dynamic Menu"
{% for item in menus.header_menu.items %}
{% if item.level > 0 %}
{{ item.name|title }}
{% else %}
{{ item.name|title }}
{% endif %}
{% endfor %}
```
Storefront Menus can be up to 3 levels, ensure your custom menu supports 2 nested menu item levels, see child and grandchild above.
| Property | Type | Description |
| --------------- | ------- | ----------------------------------------------------------------------------------- |
| `active` | Boolean | Indicates whether the link is currently active. |
| `child_active` | Boolean | Indicates whether any child link of the current link is active. |
| `child_current` | Boolean | Indicates whether the URL path matches the URL of a child link of the current link. |
| `current` | Boolean | Indicates whether the current URL path matches the URL of the link. |
| `items` | List | Contains the child items belonging to the current menu item. |
| `level` | Integer | Specifies the hierarchical level of the current menu item. |
| `name` | String | Represents the display name of the current menu item. |
| `url` | String | Denotes the URL path for the menu item's href link. |
### page [#page]
Storefront Page object details available in the `pages/page.html` template and custom page templates.
| Property | Type | Description |
| ---------------------- | ------ | -------------------------- |
| `title` | String | Page title. |
| `content` | String | Page content. |
| `get_meta_title` | String | Page SEO meta title. |
| `get_meta_description` | String | Page SEO meta description. |
### paginator [#paginator]
The `paginator` object is available on "list views" where the items to display are paginated from the backend, works in tandem with [page\_obj](#page_obj).
```jinja title="paginator"
{% if paginator.num_pages > 1 %}
{% endif %}
```
| Property | Type | Description |
| ----------- | ------- | ---------------------------------- |
| `num_pages` | Integer | Number of pages in pagination set. |
### page\_obj [#page_obj]
The `page_obj` object is available on "list views" where the items to display are paginated from the backend, works in tandem with [paginator](#paginator).
| Property | Type | Description |
| ---------------------- | ------- | --------------------- |
| `number` | Integer | Current page number. |
| `has_next` | Object | Next page object. |
| `has_previous` | Object | Previous page. |
| `next_page_number` | Integer | Next page number. |
| `previous_page_number` | Integer | Previous page number. |
### post [#post]
Blog post properties available through global [post](#posts) context and the `blog/post.html` and `blog/index.html` templates.
| Property | Type | Description |
| ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------- |
| `id` | String | The post ID. |
| `featured_image` | File | Blog post featured image file, use `.url` to access full file link. |
| `categories` | List | List of related post categories, use `.all` to return a list of categories, see [post\_category](#post_category). |
| `get_absolute_url` | String | A full path link to the blog post. |
| `title` | String | The post title. |
| `content` | String | Post content. |
| `slug` | String | Post url slug. |
| `get_meta_title` | String | Post SEO meta title. |
| `get_meta_description` | String | Post SEO meta description. |
### post\_category [#post_category]
Blog post category properties available through global [post\_categories](#post_categories) context and the `blog/post.html` and `blog/index.html` templates.
| Property | Type | Description |
| ------------------ | ------ | -------------------------------------- |
| `id` | String | Blog post category ID. |
| `name` | String | Blog post category name. |
| `get_absolute_url` | String | Blog post category canonical url link. |
### product [#product]
Product configured in the store catalogue.
| Property | Type | Description |
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `id` | String | Product ID. |
| `title` | String | Product title. |
| `get_title` | String | Product title. |
| `get_all_images` | List | List of product images, see [image](#image). |
| `get_description` | String | Product description. |
| `sku` | String | Product stock keeping unit (sku). |
| `categories` | List | List of product categories, use `.all` to return a list of categories, see [product\_category](#product_category). |
| `parent` | Object | Parent product if product is variant (child). |
| `is_child` | Boolean | Product structure indicating this is a variant product. |
| `primary_image` | File | Primary image of the product, use `.url` to access a full file link. |
| `get_absolute_url` | String | Product canonical url link. |
| `num_approved_reviews` | Integer | Count of approved product reviews. |
| `rating` | Integer | Product rating as a number between 1 and 5. |
| `reviews` | Integer | List of product reviews, see [review](#review). |
| `get_meta_title` | String | Product SEO meta title. |
| `get_meta_description` | String | Product SEO meta description. |
### product\_category [#product_category]
Product category object.
| Property | Type | Description |
| ---------------------- | ------ | ------------------------------------------------ |
| `id` | String | Category ID. |
| `name` | String | Category name. |
| `description` | String | Category description. |
| `image` | File | Category image, use `.url` to access image link. |
| `get_absolute_url` | String | Category canonical url link. |
| `get_meta_title` | String | Category SEO meta title. |
| `get_meta_description` | String | Category SEO meta description. |
### price [#price]
Product price object.
| Property | Type | Description |
| -------------- | ------ | --------------------------- |
| `currency` | String | Price currency. |
| `price` | String | Price that will be charged. |
| `price_retail` | String | Suggested retail price. |
### review [#review]
Product review object.
| Property | Type | Description |
| -------- | ------- | ------------------------------------- |
| `id` | String | Review ID. |
| `title` | String | Review title. |
| `score` | Integer | Review score. |
| `user` | Object | The customer that created the review. |
### voucher [#voucher]
Voucher object.
| Property | Type | Description |
| -------- | ------ | -------------- |
| `title` | String | Voucher title. |
| `code` | String | Voucher code. |
## View-Specific Objects [#view-specific-objects]
View-specific objects are available in the templates rendered by their corresponding views. See [Template Contexts](#template-contexts) below for which objects are available in each template.
### session [#session]
The session object is returned by the [`purchase_info_for_product`](/docs/storefront/themes/templates/tags#purchase_info_for_product) and [`purchase_info_for_line`](/docs/storefront/themes/templates/tags#purchase_info_for_line) template tags. It contains pricing and availability information for a product in the current user's currency.
```jinja title="Example Product Price with Session"
{% purchase_info_for_product request product as session %}
{% if session.price.exists %}
{% if session.price.price_retail %}
{{ session.price.price_retail|currency:session.price.currency }}
{% endif %}
{{ session.price.price|currency:session.price.currency }}
{% endif %}
{% if not session.availability.is_available_to_buy %}
Out of Stock
{% endif %}
```
**session.price**
| Property | Type | Description |
| -------------- | ------- | ---------------------------------------- |
| `exists` | Boolean | Whether a price exists for this product. |
| `price` | Decimal | The current selling price. |
| `price_retail` | Decimal | The retail/compare-at price, if set. |
| `currency` | String | The currency code for this price. |
| `excl_tax` | Decimal | The price excluding tax. |
**session.availability**
| Property | Type | Description |
| --------------------- | ------- | ------------------------------------- |
| `is_available_to_buy` | Boolean | Whether the product can be purchased. |
### variant\_form [#variant_form]
The variant selection form object available on product detail pages (`catalogue/product.html`). Used to render variant attribute selectors (size, color, etc.) for products with variants.
```jinja title="Example Variant Selection"
{% if variant_form %}
{% for field in variant_form %}
{{ field.label }}
{% render_field field class+="form-select" %}
{% endfor %}
{% endif %}
```
### filters [#filters]
The `filters` object is a list of product filter/facet objects available on category pages (`catalogue/category.html`). Filters allow customers to narrow product listings by attributes like price, color, size, etc. The `has_active_filter` boolean indicates whether any filter is currently applied.
There are three filter types: `price_range`, `boolean`, and `list`. Each type has different properties for rendering the appropriate UI.
```jinja title="Example Category Filters"
{% if filters %}
{% endif %}
```
**Common filter properties:**
| Property | Type | Description |
| --------------- | ------ | ------------------------------------------------------- |
| `type` | String | Filter type: `'price_range'`, `'boolean'`, or `'list'`. |
| `label` | String | Display label for the filter. |
| `active_values` | List | List of currently selected filter values. |
| `url_to_remove` | String | URL to remove this active filter. |
**Price range filter properties:**
| Property | Type | Description |
| ---------------------- | ------- | ------------------------------------- |
| `min_value.value` | Decimal | Current minimum price value. |
| `min_value.param_name` | String | Query parameter name for the minimum. |
| `min_value.label` | String | Display label for the minimum input. |
| `max_value.value` | Decimal | Current maximum price value. |
| `max_value.param_name` | String | Query parameter name for the maximum. |
| `max_value.label` | String | Display label for the maximum input. |
| `range_max` | Decimal | Maximum possible range value. |
**Boolean filter properties:**
| Property | Type | Description |
| ------------------------ | ------- | ----------------------------------------- |
| `true_value.param_name` | String | Query parameter name for true selection. |
| `true_value.active` | Boolean | Whether true is currently selected. |
| `true_value.count` | Integer | Number of results matching true. |
| `false_value.param_name` | String | Query parameter name for false selection. |
| `false_value.active` | Boolean | Whether false is currently selected. |
| `false_value.count` | Integer | Number of results matching false. |
**List filter value properties:**
| Property | Type | Description |
| ------------ | ------- | ----------------------------------------- |
| `param_name` | String | Query parameter name for this value. |
| `active` | Boolean | Whether this value is currently selected. |
| `count` | Integer | Number of results matching this value. |
| `value` | String | The filter value. |
| `label` | String | Display label for this value. |
### line [#line]
Cart line object available on the cart page (`templates/cart.html`) through the `formset` context. Each form in the formset exposes its cart line as `form.instance`.
```jinja title="Example Cart Line Properties"
{% for form in formset %}
{% with line=form.instance %}
{% for property in line.properties %}
{{ property.key }}: {{ property.value }}
{% endfor %}
{% endwith %}
{% endfor %}
```
| Property | Type | Description |
| ------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `properties` | List | Line item properties captured on the product page, see [Personalized Products](/docs/storefront/themes/guides/personalized-products). Property names starting with an underscore are excluded. |
**line.properties**
| Property | Type | Description |
| -------- | ------ | --------------------------------------------------------------------------------- |
| `key` | String | The property name, taken from the `properties[]` input on the product page. |
| `value` | String | The value submitted by the customer. |
## Template Contexts [#template-contexts]
All templates receive the [Global Objects](#global-objects) (`store`, `settings`, `currencies`, `languages_active_storefront`, `menus`, `products`, `product_categories`, `posts`, `post_categories`, `privacy_policy`, `terms_and_conditions`, `subscription_terms_and_conditions`, `request`, `storefront_geos`). The table below lists additional view-specific context variables passed to each template.
| Template | View-Specific Context |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `templates/index.html` | Global objects only. Use the [`where`](/docs/storefront/themes/templates/tags#where) tag to query products and categories. |
| `templates/cart.html` | `formset` (cart line forms, each `form.instance` is a [line](#line)) |
| `templates/catalogue/product.html` | `product`, `variant_form`, `interval_count_choices` |
| `templates/catalogue/index.html` | `products` (paginated), `paginator`, `page_obj` |
| `templates/catalogue/category.html` | `category`, `products` (paginated), `filters`, `has_active_filter`, `paginator`, `page_obj` |
| `templates/search.html` | `query`, `products` (paginated), `paginator`, `page_obj` |
| `templates/blog/index.html` | `posts` (paginated), `paginator`, `page_obj` |
| `templates/blog/post.html` | `post` |
| `templates/pages/page.html` | `page` |
| `templates/support/index.html` | `categories` |
| `templates/support/category.html` | `category`, `articles` |
| `templates/support/article.html` | `article` |
| `templates/reviews/index.html` | `product`, `reviews` (paginated), `paginator`, `page_obj` |
| `templates/reviews/form.html` | `product`, `form` |
| `templates/reviews/review.html` | `product`, `review` |
Use the [`purchase_info_for_product`](/docs/storefront/themes/templates/tags#purchase_info_for_product) tag in any template to get pricing and availability for a product. Use [`cart_form`](/docs/storefront/themes/templates/tags#cart_form) on product pages to generate add-to-cart forms.
**Cart and user data must use the [Storefront GraphQL API](/docs/storefront/graphql).** All storefront pages are fully cached per language and currency combination. Per-user data (cart contents, authentication state, wishlists) rendered in server-side templates would be cached and served to other visitors. Use client-side JavaScript with the GraphQL API for all cart and user interactions.
## Dashboard Cross-Reference [#dashboard-cross-reference]
Some template variables are populated from dashboard settings. Use this reference to understand where data originates when building or debugging templates.
| Template Variable | Dashboard Path | Description |
| ----------------------------------- | ------------------------------- | ------------------------------------------- |
| `store.branding.logo` | Settings > Branding | Store logo image. |
| `store.branding.icon` | Settings > Branding | Store icon/favicon image. |
| `store.branding.primary_color` | Settings > Branding | Primary brand color (HEX). |
| `store.branding.accent_color` | Settings > Branding | Accent brand color (HEX). |
| `store.name`, `store.tagline` | Settings > General | Store name and tagline. |
| `store.legal_name`, `store.address` | Settings > General | Legal details and address. |
| `menus.{menu_key}.items` | Storefront > Navigation | Navigation menu items (up to 3 levels). |
| `privacy_policy` | Settings > Policies | Privacy policy content. |
| `terms_and_conditions` | Settings > Policies | Terms and conditions content. |
| `subscription_terms_and_conditions` | Settings > Policies | Subscription T\&C content. |
| `settings.*` | Storefront > Themes > Customize | Theme settings from `settings_schema.json`. |
# Tag Reference (/docs/storefront/themes/templates/tags)
Template tags enable theme developers to include and extend templates and blocks, add logical operators, query and filter data, and much more. See all available template tags below.
### app\_asset\_url [#app_asset_url]
The `app_asset_url` tag is used to reference asset files included in app snippets.
```jinja title="app_asset_url"
```
### app\_hook [#app_hook]
The `app_hook` tag specifies a theme storefront location Apps can inject snippets into to extend storefront templates from Apps. Theme developers should ensure their templates include all available `app_hooks` to ensure compatibility with all Apps.
```jinja title="app_hook"
{% app_hook 'global_header' %}
```
**Available `app_hook` locations include:**
### add\_query\_param [#add_query_param]
The `add_query_param` tag appends or updates a query parameter on the current URL. Commonly used for building pagination links and filter URLs while preserving existing query parameters.
```jinja title="add_query_param"
{% add_query_param request 'page' page_obj.next_page_number %}
```
```jinja title="Example Pagination with add_query_param"
{% if paginator.num_pages > 1 %}
{% endif %}
```
| Argument | Description |
| ----------- | --------------------------------------------- |
| request | The current `request` context object. |
| param\_name | The query parameter name to set, eg `'page'`. |
| value | The value to assign to the parameter. |
### annotate\_form\_field [#annotate_form_field]
The `annotate_form_field` tag adds HTML attributes to a form field based on its Django form field properties (required, type, etc.). Useful for adding client-side validation and accessibility attributes.
```jinja title="annotate_form_field"
{% annotate_form_field field %}
{{ field }}
```
### boolean operators [#boolean-operators]
If tags may be used in combination with boolean operators for conditional control flow.
| Operator | Description |
| -------- | ---------------------------- |
| `and` | multiple conditions are true |
| `or` | either condition is true |
| `not` | a condition is not true |
| `in` | contained within |
| `not in` | a condition is not true |
| `is` | two values are the same |
| `is not` | two values are not the same |
| `==` | equality |
| `!=` | inequality |
| `<` | less than |
| `>` | greater than |
| `<=` | less than or equal to |
| `>=` | greater than or equal to |
### cart\_form [#cart_form]
The `cart_form` tag generates an add-to-cart form for a product. Required on every product page to enable purchasing.
```jinja title="cart_form"
{% cart_form request product 'single' as cart_form %}
```
The tag returns a form object, not rendered HTML. Loop over it to render each field in your own markup, as the Intro Bootstrap theme does through its `partials/form_fields.html` partial.
```jinja title="templates/catalogue/product.html"
{% block product_cart_form %}
{% purchase_info_for_product request product as session %}
{% if session.availability.is_available_to_buy %}
{% cart_form request product 'single' as cart_form %}
{% else %}
{% t "store.catalogue.out_of_stock" %}
{% endif %}
{% endblock %}
```
```jinja title="partials/form_fields.html"
{% if form.is_bound and not form.is_valid %}
{% t "global.error.please_check_error" %}
{% endif %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
{% endif %}
{% for field in form %}
{% include 'partials/form_field.html' with field=field style=style %}
{% endfor %}
```
| Argument | Description |
| -------------- | --------------------------------------------- |
| request | The current `request` context object. |
| product | The `product` context object. |
| quantity\_type | Accepts `'single'` or `'multiple'`. |
| variable | Assigned template variable name for the form. |
Add [line item property](/docs/storefront/themes/guides/personalized-products) inputs to this form to capture customer personalization such as an engraving or gift message.
### comment [#comment]
Ignores everything between `{% comment %}` and `{% endcomment %}`. An optional note may be inserted in the first tag. For example, this is useful when commenting out code for documenting why the code was disabled.
```jinja title="comment"
Rendered text with {{ pub_date|date:"c" }}
{% comment "Optional note" %}
Commented out text with {{ create_date|date:"c" }}
{% endcomment %}
```
### core\_js [#core_js]
The `core_js` tag outputs the platform's core JavaScript bundle. This is required in every theme's base layout and powers cart functionality, AJAX form submissions, CSRF token handling, and other platform features.
```jinja title="core_js"
{% core_js %}
```
```jinja title="Example Placement in Base Layout"
{# jQuery must be loaded before core_js #}
{% core_js %}