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.
| Parameter | Type | Default | Description |
|---|
sortBy | string | 'newest' | 'newest' or 'oldest' |
limit | number | 50 | Max 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):
| Field | Type | Description |
|---|
status | string | null | Normalized: pending, scheduled, sent, failed, skipped, cancelled, or unknown |
provider_order_id | string | null | External reference from the fulfillment provider when stored on the conversion |
integration_name | string | null | Display name of the fulfillment integration |
scheduled_at | string | null | ISO timestamp when processing was scheduled |
sent_at | string | null | ISO timestamp when the order was sent to the provider |
failed_at | string | null | ISO timestamp on failure |
cancelled_at | string | null | ISO timestamp if cancelled |
error_message | string | null | Truncated 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:
| Field | Type | Description |
|---|
order_number | string | Public order id for this shipment |
order_date | string | null | Order created_at (ISO) |
tracking_number | string | Tracking number |
tracking_url | string | Tracking link (e.g. 17track when generated) |
carrier | string | Carrier 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.
| Parameter | Type | Default | Description |
|---|
sortBy | string | 'newest' | 'newest' or 'oldest' — controls which purchase wins when the same product appears in multiple orders. |
limit | number | 50 | Max 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.
| Parameter | Type | Default | Description |
|---|
status | string | — | Optional filter: active, paused, canceled, past_due, or trial. Empty / unknown values return all. |
limit | number | 50 | Max 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:
| Property | Type | Default | Description |
|---|
code | string | — | Filter by product code in the subscription’s products array. |
status | string | '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.
| Property | Type | Default | Description |
|---|
code | string | — | Match by product code. |
status | string | 'active' | Match by status. Use 'any' to skip. |
date | string | — | ISO 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.
| Property | Type | Default | Description |
|---|
code | string | — | Filter by product code. |
status | string | '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' });
| Filter | Type | Description |
|---|
type | string | 'physical', 'digital', or 'any' |
classification | string | 'subscription', 'one-time', or 'any' |
codes | string | Comma-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:
| Field | Type | Description |
|---|
product_files | array | All 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_files | array | Subset of product_files where purpose === 'digital'. Same row shape. |
bonus_files | array | Subset of product_files where purpose === 'bonus'. Same row shape. |
digital_file | string | URL 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_file | string | URL 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_url | string | URL of the overall primary file across all purposes (is_primary DESC, then sort_order ASC). Empty string when none. |
Inside each product_files row:
| Field | Type | Description |
|---|
id | number | null | Internal file id. |
purpose | string | 'digital' or 'bonus'. |
title | string | Display title. |
description | string | Optional description. |
image | string | Optional preview image URL. |
file | string | Download URL. Always present (rows without a URL are dropped). |
kind | string | Coarse 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_order | number | Display / primary-selection order (defaults to position in the array if not stored). |
is_primary | boolean | Whether this row is the primary file for its purpose. |
metadata | object | Public metadata object (always an object, parsed from JSON when stored that way). See Metadata fields below. Internal storage pointers are intentionally not exposed. |
The metadata object on each product_files row only exposes the following fields:
| Field | Type | Description |
|---|
mime_type | string | null | MIME type of the file (e.g. 'audio/mpeg', 'application/pdf'). |
size | number | null | Size in bytes. |
checksum | string | null | Optional checksum, when one was recorded at upload. |
extension | string | Lowercase 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):
digital_files (purpose='digital')
bonus_files (purpose='bonus')
- anything left in
product_files not already picked up
- 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:
| Field | Type | Description |
|---|
url | string | Download URL (always present). |
title | string | Display title. Falls back to 'Download'. |
kind | string | One of 'audio', 'video', 'document', 'image', 'archive', 'file'. |
purpose | string | 'digital' or 'bonus'. |
extension | string | Lowercase extension without the dot (e.g. 'mp3'). |
mime_type | string | null | MIME type when known. |
size | number | null | Size in bytes when known. |
is_primary | boolean | Whether this row is the primary file for its purpose. |
sort_order | number | Display 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.
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.
| Parameter | Type | Default | Description |
|---|
blogId | number | — | Blog ID (required) |
page | number | 1 | Page number |
perPage | number | 12 | Articles 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.
| Parameter | Type | Default | Description |
|---|
blogId | number | — | Blog ID |
articleId | number | — | Article to find related content for |
limit | number | 4 | Max related articles (1–24) |
var related = getRelatedArticles(1, 42, 3);
setVariable('related_articles', related);
Purchase Links
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.
| Property | Type | Default | Description |
|---|
slug | string | required | Machine name for the entity (e.g. member-settings). Must match ^[a-z0-9][a-z0-9_-]*$. |
name | string | slug | Human-readable entity name shown in the admin UI. |
singularName | string | null | Singular label (e.g. “Member Setting”). Alias: singular_name. |
pluralName | string | null | Plural label (e.g. “Member Settings”). Alias: plural_name. |
icon | string | null | Icon name (Lucide icon, admin UI). |
color | string | null | Accent color (CSS color string). |
fields | array | [] | 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug from entity settings |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
limit | number | 50 | Max 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fieldKey | string | required | Field key (e.g. current_weight, goal_weight) |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fieldKeys | array | required | Array of field keys (e.g. ['current_weight', 'goal_weight']) |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fieldKey | string | required | Field key to set |
value | any | required | Must be present (value: 0 / false allowed) |
force | boolean | false | Skip per-key entry scan — merge into the newest entry directly |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
async | boolean | false | When 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fields | array | required | Non-empty array of { fieldKey, value } objects |
force | boolean | false | Skip per-key scan and merge into the newest entry directly |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
async | boolean | false | When 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fieldKey | string | required | Field key to store the value under |
value | any | required | The value to store (string, number, boolean, or object) |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
async | boolean | false | When 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
fieldKey | string | required | Field key to collect values for |
limit | number | 50 | Max values to return (max 200) |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked 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 }.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
async | boolean | false | When 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.
| Property | Type | Default | Description |
|---|
slug | string | required | CRM entity slug |
pipelineId | number | — | Pipeline id (or use pipelineSlug) |
pipelineSlug | string | — | Pipeline slug (or use pipelineId) |
stageId | number | — | Stage id (or use stageSlug) |
stageSlug | string | — | Stage slug (or use stageId) |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
allEntries | boolean | false | When true, updates all matching entries (not just the newest) |
async | boolean | false | When 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.
| Property | Type | Default | Description |
|---|
fromSlug | string | required | Source entity slug |
toSlug | string | required | Target entity slug (must differ from fromSlug) |
fieldMap | object | required | { sourceFieldKey: targetFieldKey } mapping — at least one pair required |
referenceType | string | 'customer' | Linked record type |
referenceId | string | session customer_id | Linked record id |
pipelineId / pipelineSlug | number / string | — | Optional pipeline to place the new entry in |
stageId / stageSlug | number / string | — | Optional stage within that pipeline |
removeFromOld | boolean | false | When true, deletes all source entries after copying |
async | boolean | false | When 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');
}