Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.elasticfunnels.io/llms.txt

Use this file to discover all available pages before exploring further.

Backend scripts expose the same data functions available in the template engine, so you can query your brand data and use the results for logic, not just display. All data functions that hit the database are asynchronous under the hood, but they look synchronous in your code — just call them and use the return value directly. Pure-computation helpers (like formatPrice, productSavings) are truly synchronous.

Customer

getCustomer()

Returns the full current customer object from the session (same as the customer global).
var c = getCustomer();
if (c && c.email) {
  console.log('Customer:', c.email);
}

getUser()

Returns a lightweight { name, email } object for the current session customer.
var user = getUser();
if (user.email) {
  setVariable('greeting', 'Welcome, ' + user.name);
}

Orders

getOrders(sortBy?, limit?)

Fetch the current customer’s orders from the database.
ParameterTypeDefaultDescription
sortBystring'newest''newest' or 'oldest'
limitnumber50Max orders to return (max 1000)
var orders = getOrders('newest', 10);

if (orders.length > 0) {
  setVariable('has_orders', true);
  setVariable('latest_order', orders[0].order_number);

  console.log('Found', orders.length, 'orders');
}
Each order object contains:
{
  order_number: 'ORD-12345',
  created_at: '2025-01-15T10:30:00Z',
  billing_address: '...',
  billing_city: '...',
  billing_state: '...',
  billing_zip: '...',
  billing_country: 'US',
  shipping_address: '...',
  shipping_city: '...',
  shipping_state: '...',
  shipping_zip: '...',
  shipping_country: 'US',
  currency: 'USD',
  customer_name: 'John Doe',
  customer_email: 'john@example.com',
  products: [
    { name: 'Premium Plan', price: 49.99, quantity: 1, image_url: '...', currency_code: 'USD' }
  ],
  tracking: [
    { tracking_number: '1Z999AA10123456784', tracking_url: '...', carrier: 'UPS' }
  ]
}

getOrder(code)

Fetch one order for the current customer. Same scope as getOrders. The code argument is required: public order ID (order_number), internal order_id, or conversion code. Returns null if not found.
var order = getOrder(request.query.order);
if (order) {
  setVariable('order', order);
}
See Showing order details for template examples.

getOrderFulfillment(code)

Returns an allowlisted fulfillment summary for a single order. Same code rules as getOrder. Use this for status labels and timestamps shown to customers. Returns null when the order exists but has no fulfillment record yet (no integration / not scheduled), or when the order is not found. Returned fields (only these; raw fulfillment_provider_response and fulfillment_order_data are never exposed):
FieldTypeDescription
statusstring | nullNormalized: pending, scheduled, sent, failed, skipped, cancelled, or unknown
provider_order_idstring | nullExternal reference from the fulfillment provider when stored on the conversion
integration_namestring | nullDisplay name of the fulfillment integration
scheduled_atstring | nullISO timestamp when processing was scheduled
sent_atstring | nullISO timestamp when the order was sent to the provider
failed_atstring | nullISO timestamp on failure
cancelled_atstring | nullISO timestamp if cancelled
error_messagestring | nullTruncated message safe for display (max 200 chars)
var f = getOrderFulfillment(request.query.order);
if (f) {
  setVariable('fulfillment', f);
}

getOrderFulfillments()

Takes no arguments. Returns all tracking-style rows for the current customer across their orders (up to the same order cap as getOrders with limit 1000, newest first). Each row includes order_number so you can link to an order detail page. Duplicate (order_number, tracking_number) pairs are removed. Returned fields per row:
FieldTypeDescription
order_numberstringPublic order id for this shipment
order_datestring | nullOrder created_at (ISO)
tracking_numberstringTracking number
tracking_urlstringTracking link (e.g. 17track when generated)
carrierstringCarrier name
var shipments = getOrderFulfillments();
setVariable('shipments', shipments);
These APIs return filtered data only. Full provider API responses and internal fulfillment payloads are not available in backend scripts.

Bonus Products

getBonusProducts()

Fetch the current customer’s bonus products — digital downloads and extras they received as part of a purchase. Returns a deduplicated list of bonus items from all the customer’s orders.
var bonuses = getBonusProducts();

if (bonuses.length > 0) {
  setVariable('bonuses', bonuses);
  setVariable('has_bonuses', true);
}
Each bonus object contains:
{
  id: 123,                        // product ID (from brand_products)
  code: 'bonus-ebook',            // product code
  title: 'Free Bonus eBook',      // product title
  description: 'A bonus guide…',  // product description
  image_url: 'https://…/img.jpg', // product image
  bonus_url: 'https://…/file',    // download URL
  price: 0,                       // price (typically 0 for bonuses)
  type: 'digital',                // product type
  classification: 'bonus'         // product classification
}
bonus_url is the public download URL exposed to backend scripts. Internally, it comes from the saved conversion line when available, and otherwise falls back to the current bonus product download URL from the catalog. Bonus products are identified by conversion line items where is_bonus = true. The function enriches each item with full product details from the catalog. If the same bonus was granted across multiple purchases, it appears only once. Scoped to the current session (email and/or session id): if the visitor is not identified, the list is empty.

getAllBonusProducts()

Returns all bonus-classified products in the brand catalog (classification: bonus). This is not limited to what the current customer earned; it does not check orders or conversions. Object shape matches getBonusProducts() (including title, bonus_url, etc.). For catalog-only rows, bonus_url is the product’s stored download URL.
var catalogBonuses = getAllBonusProducts();
setVariable('all_bonus_skus', catalogBonuses);
For the same filter but the full product shape (price, currency, merchant_product_ids, the normalized product_files array, etc.), use getProducts({ classification: 'bonus' }) instead.

getPurchasedProducts(sortBy?, limit?)

Customer-scoped helper for member-area “My Library” pages. Returns every product the current customer has purchased, fully catalog-enriched, with same-order bonuses already attached under included_bonuses[] and a pre-rolled-up downloads[] array (each entry tagged with kind). This replaces the manual recipe of:
  • calling getSessionOrders + getAllProducts + getBonusProducts separately,
  • joining order lines back to the catalog by code,
  • guessing which bonuses go with which product by matching strings in titles.
ParameterTypeDefaultDescription
sortBystring'newest''newest' or 'oldest' — controls which purchase wins when the same product appears in multiple orders.
limitnumber50Max orders considered when scanning the customer’s history (max 1000).
var purchasedProducts = getPurchasedProducts('newest');
setVariable('purchased_products', purchasedProducts);
Each purchased product contains:
{
  id: 100,
  code: 'theta-clear',
  title: 'Theta Clear',           // checkout_title preferred when present
  checkout_title: '',
  description: '...',
  image: 'https://cdn/theta.png',
  type: 'digital',
  classification: 'main',
  price: 39,                       // catalog price
  retail_price: 99,
  currency: 'USD',

  // Decorated catalog fields (same shape as getProducts)
  product_files: [ /* normalized file rows with `kind` */ ],
  digital_files: [ /* product_files filtered by type='digital' */ ],
  bonus_files:   [ /* product_files filtered by type='bonus' */ ],
  bonus_file: 'https://cdn/...',
  digital_file: 'https://cdn/...',
  primary_download_url: 'https://cdn/...',

  // Pre-rolled-up downloads — one flat list, deduped by URL, each tagged
  // with kind ('audio' | 'video' | 'document' | 'image' | 'archive' | 'file').
  // Use this instead of walking product_files / digital_files / bonus_files
  // by hand. See the productDownloads helper section above.
  downloads: [
    { url: '...', title: 'Theta Clear Vocal Guidance', kind: 'audio',
      purpose: 'digital', extension: 'mp3', mime_type: 'audio/mpeg',
      size: null, is_primary: true, sort_order: 0 },
  ],

  // Purchase context (flat — matches the fields used in member-area templates)
  order_number: 'A0FZ2V0A',
  conversion_code: 'CONV-1',
  created_at: '2026-04-28T10:00:00Z',
  line_price: 39,                  // amount paid for this line
  line_currency: 'USD',
  quantity: 1,
  has_shipping: false,             // true if the order has a shipping address

  // Bonuses received in the SAME order — every is_bonus line, no string
  // filtering. Each entry is catalog-enriched and carries its own
  // `downloads[]` rollup plus a direct `bonus_url`.
  included_bonuses: [
    {
      id: 200,
      code: 'integration-journal',
      title: 'The Theta Clear Integration Journal',
      classification: 'bonus',
      bonus_url: 'https://cdn/journal.pdf',
      downloads: [ /* same shape as above */ ],
      // ...full catalog fields
    },
    // ... all other bonus lines from the same order
  ]
}
Bonus attribution rule: every is_bonus line in an order is attached to every main line of that same order. This matches how funnels actually deliver bonuses (the bonuses come with the offer, not with one specific catalog item). It also avoids the silent-drop bug of older recipes that filtered bonuses by title keywords. Dedup rule: one entry per product (keyed by id, then code). When the same product appears in multiple orders, the order matching the sortBy direction wins (newest purchase for 'newest'). Bonuses come from the kept order.
// Member-area library — render every purchased product with its bonuses
// and downloads already prepared. No string heuristics, no manual joins.
var library = getPurchasedProducts('newest');

setVariable('library', library);
setVariable('library_count', library.length);
setVariable('has_library', library.length > 0);
Scoped to the current session: returns [] when the visitor is not identified.

Subscriptions

Customer-scoped recurring billing functions. All operations are restricted to subscriptions belonging to the current session customer (matched by email) — a script can never read or modify another customer’s subscription. Write functions return { ok: boolean, error?: string, ...rest } and never throw on validation failure. They enforce status transitions automatically (e.g. only active can be paused or skipped, only paused can be resumed, already-canceled cannot be canceled again). For full request/response shapes, content-gating examples, and ready-to-use page-based API examples, see Subscriptions (backend template engine).
Per-request cache: All read helpers (getSubscriptions, getSubscription with an object arg, hasSubscription, countSubscriptions) share a per-request cache. The database is queried only once per request — subsequent calls with different filters read from memory.

getSubscriptions(status?, limit?)

List the current customer’s subscriptions, newest first.
ParameterTypeDefaultDescription
statusstringOptional filter: active, paused, canceled, past_due, or trial. Empty / unknown values return all.
limitnumber50Max subscriptions to return (1–200).
var all = getSubscriptions();
var active = getSubscriptions('active', 25);

setVariable('subscriptions', all);
setVariable('has_active_subscription', active.length > 0);
Each subscription object contains:
{
  id: 'sub_abc123',
  status: 'active',                      // active | paused | canceled | past_due | trial
  customer_email: 'jane@example.com',
  customer_name: 'Jane Doe',
  products: [
    { code: 'monthly-plan', name: 'Monthly Plan', price: 29.99, quantity: 1, type: 'main' }
  ],
  amount: 29.99,
  currency_code: 'USD',
  tax_amount: 0,
  shipping_amount: 0,
  discount_amount: 0,
  frequency: 1,
  frequency_unit: 'month',               // day | week | month | year
  next_charge_at: '2026-05-12T00:00:00Z',
  started_at: '2026-04-12T00:00:00Z',
  paused_at: null,
  resume_at: null,
  canceled_at: null,
  cancel_reason: null,
  cycle_number: 2,
  trial_days: 0,
  first_charge_free: false,
  original_transaction_id: 'txn_xyz789',
  gateway: 'stripe',
  card_last4: '4242',
  card_brand: 'visa',
  created_at: '2026-04-12T00:00:00Z',
  updated_at: '2026-05-01T00:00:00Z'
}

getSubscription(arg)

Fetch a single subscription. Accepts either a transaction ID string or a filter object. String form — fetch by original transaction ID (existing behavior):
var sub = getSubscription(request.query.id);
if (sub) {
  setVariable('subscription', sub);
}
Object form — fetch the first matching subscription from the per-request cache:
PropertyTypeDefaultDescription
codestringFilter by product code in the subscription’s products array.
statusstring'active'Filter by status. Use 'any' to skip status filtering.
// First active subscription for a specific product
var sub = getSubscription({ code: 'monthly-plan' });

// Most recent canceled subscription (win-back flow)
var canceled = getSubscription({ code: 'monthly-plan', status: 'canceled' });
if (canceled) {
  setVariable('canceled_at', canceled.canceled_at);
  setVariable('show_winback', true);
}

// First subscription regardless of code, any status
var any = getSubscription({ status: 'any' });

hasSubscription(filter?)

Returns true if the customer has at least one subscription matching the filter, false otherwise. All fields are optional.
PropertyTypeDefaultDescription
codestringMatch by product code.
statusstring'active'Match by status. Use 'any' to skip.
datestringISO date string. When set, ignores status and instead checks whether the subscription was active on that date (started_at ≤ date and canceled_at is null or after the date).
// Does the customer have any active subscription?
var isSubscribed = hasSubscription();

// Active subscription for a specific product (content gate)
if (hasSubscription({ code: 'premium-plan' })) {
  setVariable('show_premium', true);
}

// Was the customer subscribed on a specific date? (date-based gate)
if (hasSubscription({ code: 'premium-plan', date: '2026-01-01' })) {
  setVariable('can_view_jan_content', true);
}

// Does the customer have a past-due subscription? (dunning banner)
if (hasSubscription({ status: 'past_due' })) {
  setVariable('show_billing_warning', true);
}

countSubscriptions(filter?)

Returns the number of subscriptions matching the filter. Same filter shape as hasSubscription, excluding date.
PropertyTypeDefaultDescription
codestringFilter by product code.
statusstring'active'Filter by status. Use 'any' for all statuses.
var activeCount = countSubscriptions();
setVariable('active_sub_count', activeCount);

var totalCount = countSubscriptions({ status: 'any' });
var canceledCount = countSubscriptions({ status: 'canceled' });

cancelSubscription(transactionId, reason?)

Cancel an active or paused subscription. Fires the subscription_cancel event.
var result = cancelSubscription('txn_xyz789', 'Too expensive');
if (!result.ok) {
  setVariable('error_message', result.error);
}

pauseSubscription(transactionId, resumeAt?)

Pause an active subscription. Optional resumeAt is an ISO date string for an automatic resume — invalid dates return { ok: false, error: 'Invalid resume_at date' }.
pauseSubscription('txn_xyz789');
pauseSubscription('txn_xyz789', '2026-08-01T00:00:00Z');

resumeSubscription(transactionId)

Resume a paused subscription. If the stored next_charge_at is in the past, it is automatically advanced based on the subscription’s frequency.
resumeSubscription('txn_xyz789');

skipSubscription(transactionId)

Skip the next billing cycle on an active subscription. Returns the new next charge date on success.
var result = skipSubscription('txn_xyz789');
if (result.ok) {
  setVariable('next_charge', result.next_charge_at);
}

changeSubscriptionFrequency(transactionId, interval, intervalCount)

Change the billing cadence on an active or paused subscription. interval must be one of day, week, month, year. intervalCount must be a positive integer.
var result = changeSubscriptionFrequency('txn_xyz789', 'month', 2);
// → { ok: true, frequency_unit: 'month', frequency: 2, next_charge_at: '2026-06-12T...' }

Conversions

getConversions(email)

Look up conversions (purchases) by email address.
var conversions = getConversions('john@example.com');

if (conversions.length > 0) {
  setVariable('returning_customer', true);
}
Returns an array of conversion records with fields like public_order_id, created_at, customer_email, product_name, total, currency_code, status.

Products

getProduct(code)

Fetch a single product by its product code.
var product = getProduct('premium-plan');

if (product) {
  setVariable('product_name', product.name);
  setVariable('product_price', product.price);
}

getProducts(filters?)

Fetch products with optional filters.
// All products
var all = getProducts();

// Filtered
var physical = getProducts({ type: 'physical' });
var subscriptions = getProducts({ classification: 'subscription' });
var specific = getProducts({ codes: 'plan-a,plan-b,plan-c' });
FilterTypeDescription
typestring'physical', 'digital', or 'any'
classificationstring'subscription', 'one-time', or 'any'
codesstringComma-separated product codes

getAllProducts()

Fetch all products for the brand (no filtering).
var products = getAllProducts();
console.log('Total products:', products.length);

getProductByCode(merchantCode, productCode)

Look up a product by merchant gateway code and the merchant-specific product code.
var product = getProductByCode('stripe', 'price_1234');

Product files and download URLs

Catalog products can include attached downloads. getProduct, getAllProducts, getProductByCode, and getProducts (with or without filters) all return the same normalized shape, so you can rely on the same fields in every code path. Use these fields:
FieldTypeDescription
product_filesarrayAll file rows attached to the product (always an array — never a JSON string). Each item: { id, purpose, title, description, image, file, sort_order, is_primary, metadata }. purpose is 'digital' or 'bonus' (defaults to 'digital' when missing). Rows with no file URL are filtered out.
digital_filesarraySubset of product_files where purpose === 'digital'. Same row shape.
bonus_filesarraySubset of product_files where purpose === 'bonus'. Same row shape.
digital_filestringURL of the primary digital file (the is_primary row with the lowest sort_order whose purpose is 'digital'). Falls back to the legacy digital_file column on the product if set. Empty string when none.
bonus_filestringURL of the primary bonus file (the is_primary row with the lowest sort_order whose purpose is 'bonus'). Falls back to the legacy bonus_file column, then to any primary file. Empty string when none.
primary_download_urlstringURL of the overall primary file across all purposes (is_primary DESC, then sort_order ASC). Empty string when none.
Inside each product_files row:
FieldTypeDescription
idnumber | nullInternal file id.
purposestring'digital' or 'bonus'.
titlestringDisplay title.
descriptionstringOptional description.
imagestringOptional preview image URL.
filestringDownload URL. Always present (rows without a URL are dropped).
kindstringCoarse classification derived from metadata.extension and metadata.mime_type. One of 'audio', 'video', 'document', 'image', 'archive', 'file'. Use this to branch on file type without re-parsing the URL.
sort_ordernumberDisplay / primary-selection order (defaults to position in the array if not stored).
is_primarybooleanWhether this row is the primary file for its purpose.
metadataobjectPublic metadata object (always an object, parsed from JSON when stored that way). See Metadata fields below. Internal storage pointers are intentionally not exposed.

Metadata fields

The metadata object on each product_files row only exposes the following fields:
FieldTypeDescription
mime_typestring | nullMIME type of the file (e.g. 'audio/mpeg', 'application/pdf').
sizenumber | nullSize in bytes.
checksumstring | nullOptional checksum, when one was recorded at upload.
extensionstringLowercase extension without the dot (e.g. 'mp3', 'pdf', 'docx'). Derived from the file URL when possible, with a small mime-type fallback for opaque/signed URLs. Empty string when neither source can produce one.
Internal storage pointers (provider, storage_path, brand_file_id) used to be present on this object and are now intentionally hidden — they were only useful to the platform itself and should not be relied on by scripts or templates.
Example — pick the primary bonus URL with a safe fallback, then list all digital files with their type:
var p = getProduct('my-sku');

if (p && (p.bonus_file || p.primary_download_url)) {
  setVariable('bonus_href', p.bonus_file || p.primary_download_url);
}

if (p && p.digital_files && p.digital_files.length) {
  for (var i = 0; i < p.digital_files.length; i++) {
    var row = p.digital_files[i];
    console.log(
      row.title,
      row.file,
      'kind:', row.kind,
      'ext:', row.metadata && row.metadata.extension,
      'mime:', row.metadata && row.metadata.mime_type
    );
  }
}
Example — list every bonus across a filtered product set:
var bonuses = getProducts({ classification: 'bonus' });

for (var i = 0; i < bonuses.length; i++) {
  var b = bonuses[i];
  console.log(b.title, '→', b.bonus_file || b.primary_download_url);
}
For the same field names in backend page templates (GetProduct / GetProducts / GetAllProducts), see Products, categories & shipping (backend).

productDownloads(product)

Roll up everything a member-area UI normally needs to render “what can the customer actually download for this product” into a single flat array. Replaces the recurring extractFiles / “classify by extension” gymnastics — every entry already carries kind, so you can switch on it without parsing URLs. Source order, deduplicated by URL (first occurrence wins):
  1. digital_files (purpose='digital')
  2. bonus_files (purpose='bonus')
  3. anything left in product_files not already picked up
  4. legacy scalar digital_file / bonus_file columns — surfaced only when no file rows exist, so you never get duplicates of a row that’s already in product_files
Each entry:
FieldTypeDescription
urlstringDownload URL (always present).
titlestringDisplay title. Falls back to 'Download'.
kindstringOne of 'audio', 'video', 'document', 'image', 'archive', 'file'.
purposestring'digital' or 'bonus'.
extensionstringLowercase extension without the dot (e.g. 'mp3').
mime_typestring | nullMIME type when known.
sizenumber | nullSize in bytes when known.
is_primarybooleanWhether this row is the primary file for its purpose.
sort_ordernumberDisplay order.
Example — render a member’s library entry without any classify-by-URL logic:
var p = getProduct('my-sku');
var files = productDownloads(p);

for (var i = 0; i < files.length; i++) {
  var f = files[i];
  switch (f.kind) {
    case 'audio':    console.log('🎧', f.title, f.url); break;
    case 'video':    console.log('🎬', f.title, f.url); break;
    case 'document': console.log('📄', f.title, f.url); break;
    case 'image':    console.log('🖼️', f.title, f.url); break;
    case 'archive':  console.log('📦', f.title, f.url); break;
    default:         console.log('📁', f.title, f.url);
  }
}
The same helper is available in backend page templates as ProductDownloads(product) — see Products, categories & shipping (backend).

productSavings(product)

Compute savings info from a product’s price and retail_price. Returns an object:
var product = getProduct('premium-plan');
var savings = productSavings(product);

// savings = {
//   amount: 20,
//   percent: 25,
//   amountFormatted: '$20.00',
//   percentFormatted: '25%'
// }

if (savings.percent > 0) {
  setVariable('discount_badge', 'Save ' + savings.percentFormatted);
}
Returns { amount: 0, percent: 0, amountFormatted: '', percentFormatted: '' } when there is no discount.

subscriptionSummary(product)

Returns a human-readable subscription description string.
var product = getProduct('monthly-plan');
var label = subscriptionSummary(product);
// e.g. "7-day free trial · Every 1 month"

setVariable('subscription_label', label);
Returns an empty string for non-subscription products.

formatPrice(value, currency?)

Format a numeric price with the given currency code.
var formatted = formatPrice(49.99, 'USD');
// "$49.99"
setVariable('display_price', formatted);

Courses

getCourse(courseId)

Fetch a course by ID, including modules and contents.
var course = getCourse(42);
if (course) {
  setVariable('course_title', course.title);
  setVariable('lesson_count', course.total_lesson_count);
}

getCourseBySlug(slug)

Fetch a course by its URL slug.
var course = getCourseBySlug('javascript-basics');

getModuleBySlug(courseSlug, moduleSlug)

Fetch a single module with its contents.
var module = getModuleBySlug('javascript-basics', 'variables');
if (module) {
  setVariable('module_title', module.title);
}

getCourses()

Fetch all courses for the brand with lesson and module counts.
var courses = getCourses();
setVariable('course_count', courses.length);

getCoursesByCategorySlug(categorySlug)

Filter courses by category slug.
var marketingCourses = getCoursesByCategorySlug('marketing');
setVariable('marketing_course_count', marketingCourses.length);

getCategoriesWithCourses()

Get all course categories with their courses grouped together.
var categories = getCategoriesWithCourses();
// [
//   { category_slug: 'marketing', category_name: 'Marketing', courses: [...] },
//   { category_slug: 'design', category_name: 'Design', courses: [...] },
// ]

setVariable('category_count', categories.length);

getCourseForCustomer(slug)

Fetch a single course with enrollment data for the current customer. Returns the full module/lesson tree plus completed_content_ids, progress_percent, is_assigned, and started.
var course = getCourseForCustomer('javascript-basics');
if (course && course.is_assigned) {
  setVariable('progress', course.progress_percent);
}

getCoursesForCustomer()

Fetch all courses the current customer is enrolled in. Same as getCourses(true).
var myCourses = getCoursesForCustomer();
Each course includes module_count, lesson_count, progress_percent, completed_content_ids, and is_assigned.

getCoursesForCustomerByCategorySlug(categorySlug)

Fetch the current customer’s enrolled courses, filtered by category slug. Returns an empty array if the category has no enrolled courses.
var marketing = getCoursesForCustomerByCategorySlug('marketing');
setVariable('marketing_courses', marketing);

getCategoriesWithCoursesForCustomer()

Get all course categories that contain at least one course the current customer is enrolled in, with the enrolled courses grouped per category. Useful for category-tab navigation on a member-facing courses page.
var categories = getCategoriesWithCoursesForCustomer();
// [
//   { category_slug: 'marketing', category_name: 'Marketing', courses: [...] },
//   { category_slug: 'design',    category_name: 'Design',    courses: [...] }
// ]

getCompletedLessonsCount(opts?)

Count completed lessons across all enrolled courses, or for a specific course.
var totalDone = getCompletedLessonsCount();
var courseDone = getCompletedLessonsCount({ course: 'javascript-basics' });

getCourseProgress(opts?)

Get a structured progress summary with per-course and overall completed/total lesson counts.
var progress = getCourseProgress();
// progress.completedLessons  → 12
// progress.totalLessons      → 48
// progress.courses → [{ slug, title, completedLessons, totalLessons, progressPercent }]

Blog

getBlog(blogId?)

Fetch a blog by ID. If no ID is provided, returns the default blog for the current domain.
var blog = getBlog();
if (blog) {
  setVariable('blog_name', blog.name);
}

// Or by specific ID
var specific = getBlog(5);

getBlogArticles(blogId, page?, perPage?)

Fetch published blog articles with pagination.
ParameterTypeDefaultDescription
blogIdnumberBlog ID (required)
pagenumber1Page number
perPagenumber12Articles per page (max 100)
var blog = getBlog();
if (blog) {
  var articles = getBlogArticles(blog.id, 1, 10);
  setVariable('article_count', articles.length);
  setVariable('articles', articles);
}
Each article includes a body field with the article content.

getBlogArticle(blogId, slug)

Fetch a single published article by blog ID and slug.
var article = getBlogArticle(1, 'getting-started');
if (article) {
  setVariable('article_title', article.title);
  setVariable('article_body', article.body);
}

getBlogCategories(blogId)

Get category summaries for a blog’s published articles.
var categories = getBlogCategories(1);
setVariable('blog_categories', categories);

getRelatedArticles(blogId, articleId, limit?)

Fetch related articles for a given article.
ParameterTypeDefaultDescription
blogIdnumberBlog ID
articleIdnumberArticle to find related content for
limitnumber4Max related articles (1–24)
var related = getRelatedArticles(1, 42, 3);
setVariable('related_articles', related);
These functions generate URLs for the payment flow. They are synchronous.

buy(productCode)

Generate a purchase link for a product.
var link = buy('premium-plan');
setVariable('buy_url', link);

upsell(productCode)

Generate an upsell link (one-click purchase for existing customers).
var link = upsell('vip-addon');
setVariable('upsell_url', link);

downsell(productCode)

Generate a downsell link (same mechanics as upsell).
var link = downsell('basic-addon');
setVariable('downsell_url', link);

decline(productCode)

Generate a decline link (skip the upsell offer and proceed).
var link = decline('vip-addon');
setVariable('decline_url', link);

upsellUpgrade(oldSku, newSku)

Generate a subscription upgrade link. Produces a JavaScript action URL that calls the server API to change the customer’s subscription product. See Subscription Upsells.
var link = upsellUpgrade('monthly_basic', 'monthly_premium');
setVariable('upgrade_url', link);

upsellCancel()

Generate a subscription cancellation link. Produces a JavaScript action URL that calls the server API to cancel the customer’s subscription.
var link = upsellCancel();
setVariable('cancel_url', link);

Assets

asset(path)

Generate a full CDN URL for a brand asset.
var cssUrl = asset('/main.css');
var imgUrl = asset('/images/hero.jpg');

setVariable('css_url', cssUrl);
Automatically appends a cache-busting parameter when ?nc, ?no_cache, or ?preview_key is present in the request.

Visitor Control

whitelistVisitor()

Whitelist the current visitor by setting an encrypted cookie. Whitelisted visitors bypass access restrictions.
var customer = getCustomer();
if (customer.email === 'admin@example.com') {
  whitelistVisitor();
}

blacklistVisitor()

Blacklist the current visitor by setting an encrypted cookie.
if (request.query.block === 'true') {
  blacklistVisitor();
  redirect('/blocked');
}

Session Items

These functions read and write custom session items that are compatible with the template engine’s setSessionItem / getSessionItem functions. Values are stored with a custom_ prefix and base64-encoded. Use these when you need to share data between backend scripts and template-engine expressions on the same page or across pages within the same session.

setSessionItem(key, value)

Store a custom value in the session. Keys are sanitized to [a-zA-Z0-9_]. Values over 64KB are silently dropped. Pass null to delete.
setSessionItem('preferred_plan', 'premium');
setSessionItem('quiz_score', '85');

getSessionItem(key)

Retrieve a custom session item. Returns an empty string if not found.
var plan = getSessionItem('preferred_plan');
if (plan === 'premium') {
  setVariable('show_premium_banner', true);
}

clearSessionItem(key)

Remove a custom session item.
clearSessionItem('preferred_plan');
For raw session access (without the custom_ prefix and base64 encoding), use session.get(key) and session.set(key, value) from the Actions page.

CRM Data

Entries vs. Fields: An entry is a single record or document stored in the CRM (like a row in a database, representing e.g. a specific check-in for a customer). A field (fieldKey) is a specific piece of data that lives inside an entry (like a column, e.g. “current_weight”).
CRM entries are stored per brand. Every read/write is scoped by:
  • brand_id — from the session (always applied server-side)
  • slug — the CRM entity machine name from entity settings (e.g. weight-tracker), required on every call. This is not the same as referenceType (customer, order, …).
  • reference_type + reference_id — which record the entry is attached to. Defaults: referenceType'customer', referenceId → current session customer_id when omitted or null.
Pass a single options object only (positional arguments are not supported). Missing slug or other required keys throws TypeError. Unknown slug for the brand also throws. Invalid field values still return false / null / [] after sanitization where applicable. Slug aliases: crmEntitySlug, crm_entity_slug. Reference aliases: reference_type, reference_id. Field key: fieldKey or field_key. Write functions (setCrmField, setCrmFields, addCrmFieldValue, clearCrmEntries) accept an optional async parameter (default false). When true, the write runs in the background — faster but with eventual consistency. Read functions are always synchronous.
getCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight' });

getCrmFields({ slug: 'weight-tracker', fieldKeys: ['current_weight', 'goal_weight'] });

setCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight', value: 182, force: true });

setCrmFields({
  slug: 'weight-tracker',
  fields: [
    { fieldKey: 'current_weight', value: 182 },
    { fieldKey: 'goal_weight', value: 165 }
  ]
});

// Another record type / id
setCrmField({ slug: 'weight-tracker', referenceType: 'order', referenceId: '12345', fieldKey: 'status', value: 'shipped' });

ensureCrmEntity({ slug, name?, fields?, ... })

Idempotently provisions a CRM entity (and optionally seeds its field schema). Safe to call on every page render — existing entities and fields are never overwritten, only missing keys are inserted. Use this at the top of a backend script in templates that ship to many brands so the CRM entity is auto-created on first use, without requiring an admin to pre-configure it.
PropertyTypeDefaultDescription
slugstringrequiredMachine name for the entity (e.g. member-settings). Must match ^[a-z0-9][a-z0-9_-]*$.
namestringslugHuman-readable entity name shown in the admin UI.
singularNamestringnullSingular label (e.g. “Member Setting”). Alias: singular_name.
pluralNamestringnullPlural label (e.g. “Member Settings”). Alias: plural_name.
iconstringnullIcon name (Lucide icon, admin UI).
colorstringnullAccent color (CSS color string).
fieldsarray[]Optional field schema. Each item: { key, label?, type?, options?, validation_rules?, visible_to_team_ids? }.
Field type values: text, textarea, email, tel, url, number, integer, decimal, currency, date, datetime, time, select, multiselect, checkbox, boolean, radio, json, rich_text, wysiwyg (rich text fields: values are HTML strings). For select/multiselect/radio pass options: ['a','b'] or [{ value: 'a', label: 'A' }]. Returns:
{
  id: 42,
  slug: 'member-settings',
  created: true,        // false if entity already existed
  fields_added: 3,      // newly inserted fields
  fields_skipped: 2     // already existed (left untouched)
}
Example:
ensureCrmEntity({
  slug: 'member-settings',
  name: 'Member Settings',
  icon: 'settings',
  fields: [
    { key: 'preferred_name', label: 'Preferred name', type: 'text' },
    { key: 'time_zone',      label: 'Time zone',      type: 'select',
      options: ['UTC', 'Europe/London', 'America/New_York'] },
    { key: 'marketing_opt_in', label: 'Marketing opt-in', type: 'checkbox' },
  ],
});

// Now safe to read/write — the entity definitely exists.
var name = getCrmField({ slug: 'member-settings', fieldKey: 'preferred_name' });
Limits: max 5 ensureCrmEntity calls per page execution. Exceeding the limit returns { id: null, created: false, error: 'rate_limited' }.

getCrmEntries({ slug, referenceType?, referenceId?, limit? })

Fetch CRM entries for one entity slug and one reference.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug from entity settings
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
limitnumber50Max entries (max 200)
var entries = getCrmEntries({ slug: 'weight-tracker', limit: 50 });

for (var i = 0; i < entries.length; i++) {
  console.log(entries[i].title, entries[i].values_flat);
}
Each entry object contains:
{
  id: 'abc123',
  reference_type: 'customer',
  reference_id: 'xyz789',
  values_flat: {
    current_weight: 182,
    goal_weight: 165,
    last_checkin: '2026-03-15'
  },
  created_at: '2026-03-15T10:00:00Z',
  updated_at: '2026-03-20T14:30:00Z'
}

getCrmField({ slug, fieldKey, referenceType?, referenceId? })

Reads one field from the first matching entry for that slug + reference.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldKeystringrequiredField key (e.g. current_weight, goal_weight)
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
var weight = getCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight' });

if (weight && Number(weight) <= 165) {
  setVariable('show_goal_reached', true);
}

getCrmFields({ slug, fieldKeys, referenceType?, referenceId? })

Reads multiple fields from the first matching entries for that slug + reference. Returns an object mapping fieldKey -> value.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldKeysarrayrequiredArray of field keys (e.g. ['current_weight', 'goal_weight'])
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
var fields = getCrmFields({ slug: 'weight-tracker', fieldKeys: ['current_weight', 'goal_weight'] });

if (fields.current_weight && Number(fields.current_weight) <= 165) {
  setVariable('show_goal_reached', true);
}

setCrmField({ slug, fieldKey, value, force?, referenceType?, referenceId?, async? })

Updates an existing entry’s field or creates a new entry under that entity slug and reference.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldKeystringrequiredField key to set
valueanyrequiredMust be present (value: 0 / false allowed)
forcebooleanfalseSkip per-key entry scan — merge into the newest entry directly
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
asyncbooleanfalseWhen true, write runs in background (faster, eventual consistency)
setCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight', value: 182 });
setCrmField({ slug: 'weight-tracker', fieldKey: 'last_checkin', value: new Date().toISOString() });
Performance tip: When setting multiple fields in one script, pass force: true. Without it, each call searches all entries (up to 100) to find the document containing that key. With force, it fetches only the newest entry (limit 1) and merges the field into it — much faster for sequential writes:
setCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight', value: 182, force: true });
setCrmField({ slug: 'weight-tracker', fieldKey: 'goal_weight', value: 165, force: true });
setCrmField({ slug: 'weight-tracker', fieldKey: 'last_checkin', value: today, force: true });

setCrmFields({ slug, fields, force?, referenceType?, referenceId?, async? })

Sets multiple field values for the same CRM entity + reference in one read/write cycle. This is the preferred API when one script needs to write several fields at once.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldsarrayrequiredNon-empty array of { fieldKey, value } objects
forcebooleanfalseSkip per-key scan and merge into the newest entry directly
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
asyncbooleanfalseWhen true, write runs in background
setCrmFields({
  slug: 'weight-tracker',
  fields: [
    { fieldKey: 'current_weight', value: 182 },
    { fieldKey: 'goal_weight', value: 165 },
    { fieldKey: 'last_checkin', value: today }
  ]
});
Snake_case keys are also accepted inside fields:
setCrmFields({
  slug: 'affiliate-requests',
  referenceType: 'affiliate_request',
  referenceId: 'leon@elasticfunnels.io',
  fields: [
    { field_key: 'affiliate_experience', value: 'Wellness' },
    { field_key: 'niche', value: 'Health' }
  ]
});
Behavior matches setCrmField:
  • If an existing entry for the same reference already contains one of the requested keys, that entry is updated.
  • If the keys are new but the reference already has entries, the newest entry is reused.
  • A new CRM entry is created only when that reference has no entry yet.

addCrmFieldValue({ slug, fieldKey, value, referenceType?, referenceId?, async? })

Creates a new CRM entry with the given field value. Unlike setCrmField which upserts a single value per key, this always creates a new entry — enabling multiple values for the same key over time.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldKeystringrequiredField key to store the value under
valueanyrequiredThe value to store (string, number, boolean, or object)
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
asyncbooleanfalseWhen true, write runs in background
addCrmFieldValue({
  slug: 'weight-tracker',
  fieldKey: 'weighin_history',
  value: { date: today, weight: 182, notes: 'post-workout' }
});

getCrmFieldValues({ slug, fieldKey, limit?, referenceType?, referenceId? })

Collects all values for a given field key across CRM entries. Returns an array of the raw values (string, number, object, etc.), newest first.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
fieldKeystringrequiredField key to collect values for
limitnumber50Max values to return (max 200)
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
var history = getCrmFieldValues({ slug: 'weight-tracker', fieldKey: 'weighin_history', limit: 10 });

for (var i = 0; i < history.length; i++) {
  console.log(history[i].date, history[i].weight);
}

clearCrmEntries({ slug, referenceType?, referenceId?, async? })

Deletes all CRM entries for the given entity slug and reference. Returns { ok: boolean, deleted: number }.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
asyncbooleanfalseWhen true, deletes run in background
var result = clearCrmEntries({ slug: 'weight-tracker' });
console.log(result.deleted, 'entries removed');

moveCrmLead({ slug, pipelineId?, pipelineSlug?, stageId?, stageSlug?, referenceType?, referenceId?, allEntries?, async? })

Moves a CRM entry to a specific pipeline and stage. Either pipelineId or pipelineSlug is required; either stageId or stageSlug is required.
PropertyTypeDefaultDescription
slugstringrequiredCRM entity slug
pipelineIdnumberPipeline id (or use pipelineSlug)
pipelineSlugstringPipeline slug (or use pipelineId)
stageIdnumberStage id (or use stageSlug)
stageSlugstringStage slug (or use stageId)
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
allEntriesbooleanfalseWhen true, updates all matching entries (not just the newest)
asyncbooleanfalseWhen true, update runs in background
Returns { ok, moved, totalMatched, pipeline_id, stage_id } on success, or { ok: false, moved: 0, error }.
var result = moveCrmLead({ slug: 'deals', pipelineSlug: 'sales', stageSlug: 'qualified' });

if (!result.ok) {
  console.log('Move failed:', result.error);
}

moveCrmToStage({ slug, stageId?, stageSlug?, pipelineId?, pipelineSlug?, referenceType?, referenceId?, allEntries?, async? })

Moves a CRM entry to a specific stage. The pipeline is resolved from the stage automatically — only stageId or stageSlug is strictly required (pipeline id/slug can narrow the lookup if stage slugs are not unique across pipelines).
moveCrmToStage({ slug: 'deals', stageSlug: 'won' });

moveCrmToPipeline({ slug, pipelineId?, pipelineSlug?, stageId?, stageSlug?, referenceType?, referenceId?, allEntries?, async? })

Moves a CRM entry into a pipeline. The entry lands on the pipeline’s first stage unless a specific stage is provided.
moveCrmToPipeline({ slug: 'deals', pipelineSlug: 'onboarding' });

moveCrmToEntity({ fromSlug, toSlug, fieldMap, referenceType?, referenceId?, pipelineId?, pipelineSlug?, stageId?, stageSlug?, removeFromOld?, async? })

Copies field values from one CRM entity to another for the same reference. Use this to promote a lead to a deal, or migrate data between entity types.
PropertyTypeDefaultDescription
fromSlugstringrequiredSource entity slug
toSlugstringrequiredTarget entity slug (must differ from fromSlug)
fieldMapobjectrequired{ sourceFieldKey: targetFieldKey } mapping — at least one pair required
referenceTypestring'customer'Linked record type
referenceIdstringsession customer_idLinked record id
pipelineId / pipelineSlugnumber / stringOptional pipeline to place the new entry in
stageId / stageSlugnumber / stringOptional stage within that pipeline
removeFromOldbooleanfalseWhen true, deletes all source entries after copying
asyncbooleanfalseWhen true, write runs in background
Returns { ok, moved, removed_from_old } where moved is the number of fields copied.
var result = moveCrmToEntity({
  fromSlug: 'leads',
  toSlug: 'deals',
  fieldMap: {
    lead_score: 'initial_score',
    source: 'lead_source'
  },
  pipelineSlug: 'sales',
  stageSlug: 'new',
  removeFromOld: false
});

if (result.ok) {
  console.log(result.moved, 'fields copied');
}