# Next Commerce: Campaigns > Campaign funnels: the Campaigns API, Campaign Page Kit, starter templates, and the Admin API surface for campaigns and offers. Campaign orders are store orders. This is one of 6 domain bundles derived from the platform capability map at https://developers.nextcommerce.com/capabilities.json (generated 2026-09-03). The other bundles are listed at https://developers.nextcommerce.com/llms.txt. Merchant and operator guides are on https://docs.nextcommerce.com; the changelog is https://docs.nextcommerce.com/changelog. The Admin API contract is the OpenAPI file at https://developers.nextcommerce.com/api/admin/2024-04-01.yaml. ## Capabilities ### Campaigns (id: campaigns) Campaigns are custom checkout funnels (landing, checkout, upsell, receipt pages) backed by the CORS-enabled Campaigns API, built with the Campaign Page Kit and starter templates. A campaign's checkout is separate from the storefront checkout, but its orders are ordinary store orders that share the orders list, inventory, and subscriptions. Status: available. Audiences: merchant, developer. Links verified: 2026-09-03. Merchant and operator guides (docs.nextcommerce.com): - https://docs.nextcommerce.com/docs/apps/campaigns-app - https://docs.nextcommerce.com/docs/apps/campaigns-app/build-campaign-packages - https://docs.nextcommerce.com/docs/apps/campaigns-app/campaigns-offers-and-discounts - https://docs.nextcommerce.com/docs/apps/campaigns-app/campaign-analytics Developer guides (developers.nextcommerce.com): - https://developers.nextcommerce.com/docs/campaigns - https://developers.nextcommerce.com/docs/campaigns/page-kit - https://developers.nextcommerce.com/docs/campaigns/templates - https://developers.nextcommerce.com/docs/campaigns/api - https://developers.nextcommerce.com/docs/campaigns/admin-api Admin API operations, version 2024-04-01 (20): - [GET /campaigns/ — List every campaign belonging to the store.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsList) - [POST /campaigns/ — Create a campaign's settings.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsCreate) - [GET /campaigns/{id}/ — Retrieve a campaign's settings.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsRetrieve) - [PATCH /campaigns/{id}/ — Partial update a campaign's settings.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPartialUpdate) - [DELETE /campaigns/{id}/ — Delete a campaign's settings.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsDestroy) - [GET /campaigns/{id}/offers/ — List every offer on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersList) - [POST /campaigns/{id}/offers/ — Create an offer on the campaign, with its condition and benefit.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersCreate) - [GET /campaigns/{id}/offers/{offerId}/ — Retrieve an offer on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) - [PATCH /campaigns/{id}/offers/{offerId}/ — Partially update an offer on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate) - [DELETE /campaigns/{id}/offers/{offerId}/ — Delete an offer on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersDestroy) - [GET /campaigns/{id}/packages/ — List every packages on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesList) - [POST /campaigns/{id}/packages/ — Create a package on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesCreate) - [GET /campaigns/{id}/packages/{packageId}/ — Retrieve a package on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesRetrieve) - [PATCH /campaigns/{id}/packages/{packageId}/ — Partially update a package on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesPartialUpdate) - [DELETE /campaigns/{id}/packages/{packageId}/ — Delete a package on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesDestroy) - [GET /campaigns/{id}/shipping-methods/ — List every shipping method on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsList) - [POST /campaigns/{id}/shipping-methods/ — Create a shipping method on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsCreate) - [GET /campaigns/{id}/shipping-methods/{shippingMethodId}/ — Retrieve a shipping method on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsRetrieve) - [PATCH /campaigns/{id}/shipping-methods/{shippingMethodId}/ — Partially update a shipping method on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) - [DELETE /campaigns/{id}/shipping-methods/{shippingMethodId}/ — Delete a shipping method on the campaign.](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsDestroy) AI agent skills: - [next-campaigns-setup](https://github.com/NextCommerceCo/skills/tree/main/next-campaigns-setup) ## Pages The full text of every developer page cited above, in the order listed. # Getting Started (https://developers.nextcommerce.com/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 > **Tip (AI-ready out of the box):** 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. > **Note:** 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. > **Note (🎉 You did it!):** 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](https://developers.nextcommerce.com/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 →**](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/docs/campaigns/admin-api#add-offers-to-a-campaign). The Cart API returns the same condition and benefit fields on each offer from [campaignRetrieve](https://developers.nextcommerce.com/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 | > **Tip:** Add `localhost` and any staging domains as **development** domains so you can test the full checkout flow without polluting analytics data. > **Warning:** 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" } } ``` > **Note:** 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. > **Tip (Keep your campaign analytics clean):** 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 (https://developers.nextcommerce.com/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 ``` > **Note:** 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 > **Note:** 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` → `<slug>/_layouts/base.html` * `page_layout: custom.html` → `<slug>/_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 <h1>{{ campaign.name }}</h1> <p>Contact: {{ campaign.support_email }}</p> ``` 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 <h2>{{ campaign.custom_headline }}</h2> ``` ### 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 (`/<slug>/`). Accepts a page name like `presell` or `landing.html` — the path is normalized to `/<slug>/<page>/`. A warning is shown if the page doesn't exist under `src/<slug>/`. | ### `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" %} <!-- Google Tag Manager --> <script>...</script> {% 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. > **Tip:** 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 <script src="{{ 'config.js' | campaign_asset }}"></script> <!-- Output: /starter/config.js --> <link href="{{ 'css/custom.css' | campaign_asset }}" rel="stylesheet"> <!-- Output: /starter/css/custom.css --> <img src="{{ 'images/logo.png' | campaign_asset }}" alt="Logo"> <!-- Output: /starter/images/logo.png --> ``` **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 <a href="{{ 'checkout.html' | campaign_link }}">Checkout</a> <!-- Output: /starter/checkout/ --> <meta name="next-success-url" content="{{ next_url | campaign_link }}"> <!-- Output: /starter/upsell/ --> <button data-next-url="{{ 'upsell.html' | campaign_link }}">Continue</button> <!-- Output: /starter/upsell/ --> ``` 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/<campaign>/` 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. > **Warning:** 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` | > **Note:** 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/<slug>/_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 (https://developers.nextcommerce.com/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. # Campaign Cart API (https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/docs/admin-api/guides/external-checkout). > **Tip (Recommended: use the Campaign Cart SDK):** For most projects, start with the **[Campaigns Getting Started guide](https://developers.nextcommerce.com/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. > **Note (Looking to create campaigns, not sell through them?):** 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](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/docs/campaigns#packages) and [Concepts → Offers](https://developers.nextcommerce.com/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`](https://developers.nextcommerce.com/docs/campaigns/api/campaigns/campaignRetrieve). See [Concepts → Offers](https://developers.nextcommerce.com/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 <script async src="https://campaigns.apps.29next.com/js/v1/campaign/"></script> <script> window.addEventListener('load', function () { nextCampaign.config({ apiKey: "<YOUR CAMPAIGN API KEY>", }) nextCampaign.event('page_view', { title: document.title, url: window.location.href }); }); </script> ``` ### 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": '<YOUR CAMPAIGN API KEY>' // Campaign API Key }, body: JSON.stringify(payload), }); const result = await response.json(); console.log(result); // subtotal, total, total_discount, offer_discounts, voucher_discounts, lines ``` > **Note (Calculating on upsell pages):** 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": '<YOUR CAMPAIGN API KEY>' // 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. > **Note (The `success_url` Explained):** 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": '<YOUR CAMPAIGN API KEY>' // Campaign API Key }, body: JSON.stringify(payload), }); const result = await response.json(); console.log(result); // Show result in console ``` > **Warning (Send a Payment Failure URL):** 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. > **Note (iFrame Payment Form):** Bankcard payments require using the [iFrame Payment Form](https://developers.nextcommerce.com/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/). > **Warning (You Must Handle the Order Create Response):** * 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`. > **Tip (Handle APM Redirect Flow Declines):** 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 = '<YOUR ORDER REF ID>' const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/' + refId + '/', { method: 'GET', headers: { 'Content-Type': 'application/json', "Authorization": '<YOUR CAMPAIGN API KEY>' // 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 = '<YOUR ORDER REF ID>' 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": '<YOUR CAMPAIGN API KEY>' // 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 = '<YOUR ORDER REF ID>' const response = await fetch('https://campaigns.apps.29next.com/api/v1/orders/' + refId + '/', { method: 'GET', headers: { 'Content-Type': 'application/json', "Authorization": '<YOUR CAMPAIGN API KEY>' // Campaign API Key } }); const result = await response.json(); console.log(result); // Show result in console ``` # Campaigns Admin API (https://developers.nextcommerce.com/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 > **Warning (Never Call the Admin API From a Campaign Page):** Admin API tokens have full access to your store, so they belong on a server. Your funnel pages call the [Campaign Cart API](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/docs/admin-api/reference/products/productsList) and [productsVariantList](https://developers.nextcommerce.com/docs/admin-api/reference/products/productsVariantList) | | `gateways:read` | Read gateway groups and the payment method codes a campaign can enable, using [gatewayGroupsList](https://developers.nextcommerce.com/docs/admin-api/reference/payments/gatewayGroupsList) | See [Admin API permissions](https://developers.nextcommerce.com/docs/admin-api/permissions) for the full scope list and [Admin API → Getting Started](https://developers.nextcommerce.com/docs/admin-api) for creating an OAuth app. > **Note (Only Available on 2024-04-01 API Version):** 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](https://developers.nextcommerce.com/docs/admin-api/reference/payments/gatewayGroupsList) endpoint to get its `id` and the payment methods it supports. 2. Create the campaign using the [campaignsCreate](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsCreate) endpoint. 3. Add a package for each sellable item using the [campaignsPackagesCreate](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/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 } ``` > **Tip (Use Offers for Quantity Tiers):** 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](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/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 } ``` ### Repricing Across Currencies [#repricing-across-currencies] [campaignsPackagesCreate](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesCreate) takes a single `price` and `price_recurring`. [campaignsPackagesPartialUpdate](https://developers.nextcommerce.com/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. > **Note (Read Prices Back From the Response):** `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](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/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" } } ``` > **Note (Package IDs Read Back as Packages):** `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](https://developers.nextcommerce.com/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 } ``` > **Warning (Name the Source Currency When Recalculating):** 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](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/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. > **Warning (Sub-Resource Lists Are Not Paginated):** Unlike [campaignsList](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsList), [campaignsPackagesList](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesList) and [campaignsShippingMethodsList](https://developers.nextcommerce.com/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](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsDestroy) deletes a campaign's settings and [campaignsPackagesDestroy](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesDestroy) removes a single package. > **Warning (Check What Still Points at the Campaign First):** 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. > **Note (Domains Are Configured in the Dashboard):** Authorized domains are not managed through this API. Configure them in your store's Campaign Settings, see [Concepts → Domains](https://developers.nextcommerce.com/docs/campaigns#domains). ## Endpoints [#endpoints] Full request and response detail lives in the Admin API reference. ### Campaigns [#campaigns] | Operation | Endpoint | | -------------------------------------------------------------------------------------- | ----------------------------------- | | [Campaigns List](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsList) | `GET /api/admin/campaigns/` | | [Campaigns Create](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsCreate) | `POST /api/admin/campaigns/` | | [Campaigns Retrieve](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsRetrieve) | `GET /api/admin/campaigns/{id}/` | | [Campaigns Partial Update](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPartialUpdate) | `PATCH /api/admin/campaigns/{id}/` | | [Campaigns Destroy](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsDestroy) | `DELETE /api/admin/campaigns/{id}/` | ### Offers [#offers] | Operation | Endpoint | | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | [Campaigns Offers List](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersList) | `GET /api/admin/campaigns/{id}/offers/` | | [Campaigns Offers Create](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersCreate) | `POST /api/admin/campaigns/{id}/offers/` | | [Campaigns Offers Retrieve](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) | `GET /api/admin/campaigns/{id}/offers/{offerId}/` | | [Campaigns Offers Partial Update](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate) | `PATCH /api/admin/campaigns/{id}/offers/{offerId}/` | | [Campaigns Offers Destroy](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsOffersDestroy) | `DELETE /api/admin/campaigns/{id}/offers/{offerId}/` | ### Packages [#packages] | Operation | Endpoint | | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | [Campaigns Packages List](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesList) | `GET /api/admin/campaigns/{id}/packages/` | | [Campaigns Packages Create](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesCreate) | `POST /api/admin/campaigns/{id}/packages/` | | [Campaigns Packages Retrieve](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesRetrieve) | `GET /api/admin/campaigns/{id}/packages/{packageId}/` | | [Campaigns Packages Partial Update](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesPartialUpdate) | `PATCH /api/admin/campaigns/{id}/packages/{packageId}/` | | [Campaigns Packages Destroy](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsPackagesDestroy) | `DELETE /api/admin/campaigns/{id}/packages/{packageId}/` | ### Shipping Methods [#shipping-methods] | Operation | Endpoint | | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | [Campaigns Shipping Methods List](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsList) | `GET /api/admin/campaigns/{id}/shipping-methods/` | | [Campaigns Shipping Methods Create](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsCreate) | `POST /api/admin/campaigns/{id}/shipping-methods/` | | [Campaigns Shipping Methods Retrieve](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsRetrieve) | `GET /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` | | [Campaigns Shipping Methods Partial Update](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) | `PATCH /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` | | [Campaigns Shipping Methods Destroy](https://developers.nextcommerce.com/docs/admin-api/reference/campaigns/campaignsShippingMethodsDestroy) | `DELETE /api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/` |