Extending
Three seams connect other code to the Marketing module: the @antelopejs/interface-dms-marketing interface for backend modules, the tracker's JavaScript API for visitor-site code, and the HTTP API.
Backend modules: the marketing interface
Every /modules/ page of the DMS is reserved to the platform owner. To show marketing data to tenant users, build your own page and read it through the interface. Declare it in optionalDependencies: calls reject when no module implements it, so catch and degrade.
pnpm add --save-optional @antelopejs/interface-dms-marketing
import { GetClickHeatmap } from "@antelopejs/interface-dms-marketing";
export async function pricingHeatmap(tenantId: string) {
try {
return await GetClickHeatmap({
tenantId, // an authenticated and authorized tenant id only
path: "/pricing",
from: "2026-09-01T00:00:00.000Z",
to: "2026-09-30T23:59:59.999Z",
});
} catch {
return null; // The page can work without analytics.
}
}
The consumer reference defines the query, result types, date limits, element anchors and tenant boundaries. Resolve an element anchor against the rendered page when possible, and show an unavailable state separately from a successful heatmap with no clicks.
.meta({ name, description, icon }).Shipping content from code
A project ships its websites, funnels and page snapshots as code, and a demo or a migration its history, through the same interface — never by writing the module's tables:
This global demo explicitly selects "default". All four content calls require a nonempty tenantId without surrounding whitespace, in TypeScript and at runtime; there is no fallback. start() runs per process, not per tenant creation. For new SaaS tenants, the application should register a durable created consumer before startup reconciliation (normally during construct(), without awaiting marketing calls there). See the lifecycle integration contract for operation deduplication, durable resource bindings and receipts. For existing tenants, use an inventory and the committed-provisioning gate instead of treating created as a backfill API. Marketing does not depend on SaaS or register consumers itself; callers still own authorization and readiness.
import {
EnsureFunnel,
EnsureWebsite,
ImportAnalytics,
} from "@antelopejs/interface-dms-marketing";
export async function start() {
const tenantId = "default"; // Explicit global demo only.
await EnsureWebsite({ tenantId, id: "shop-site", name: "Shop", domain: "shop.example.com" });
await EnsureFunnel({
tenantId,
id: "shop-quote-request",
websiteId: "shop-site",
name: "Quote request",
steps: [
{ kind: "url", value: "/pricing" },
{ kind: "custom", value: "quote_submitted" },
],
});
await ImportAnalytics({
tenantId,
importId: "shop-history",
websiteId: "shop-site",
sessions: [
{
visitor: "visitor-1",
events: [
{ kind: "pageview", at: "2026-10-01T09:00:00Z", url: "https://shop.example.com/pricing" },
{ kind: "custom", at: "2026-10-01T09:02:00Z", url: "https://shop.example.com/pricing", name: "quote_submitted" },
],
},
],
});
}
EnsureWebsiteandEnsureFunnelare idempotent and keyed by the id you choose:created, thenunchangedon every later boot;updatedwhen your declaration changed and the stored content still matches its hash;kepton owner divergence or concurrent change. Both refresh and hash reset compare the observed state at write time on MongoDB. A successful divergence reset clears the hash permanently; contention alone leaves it intact. See the reference for adapter requirements and state-comparison limits.EnsurePageSnapshot({ tenantId, websiteId, url, html, viewport, document })declares the backdrop of a page's heatmap when the tracker cannot capture it (a page not online, imported history). It is keyed by the page path and the layout ofviewport, sanitized like a capture, and needs a website withsnapshotsEnabled. A capture of the live page replaces it and is never overwritten (kept).ImportAnalyticsreplays history through the live collection pipeline with its own timestamps. It runs once perimportIdand website; later calls with that id write nothing and answerin_progresswhile the first is replaying, thenskipped(orfailedif it failed). Split larger histories into chronological windows for the entire website. Concurrent imports with different ids for the same website reject, and an interrupted replay blocks further imports pending reconciliation. See the reference for the failure and rollout contract.- Await them from
start()or later; a call made earlier waits for the module and its database.
The consumer reference has the contracts, the exact declaration rule and the import's limits.
Visitor sites: the tracker API
The tracker installs window.dmsMarketing on the tracked site. Guard every call with ?.: the object is absent when the script is blocked or fails to load.
window.dmsMarketing?.track("signup_submitted", { plan: "pro" });
window.dmsMarketing?.variation("hero-cta").then((arm) => {
if (arm === "bold-cta") showAlternateHero();
});
track(name, data?)reports a custom event: a name up to 50 characters, optional data up to 4 KiB serialized. Custom events feed the overview and can be funnel steps.variation(key)resolves to the arm the backend assigned this visitor, and reports the exposure.nullmeans "render the control".exposure(experiment, variation)reports an exposure for integrations that assign arms on their own.
Mark elements that page snapshots must never capture with data-dms-marketing-mask (text masked) or data-dms-marketing-block (replaced by an empty box). See Tracker & custom events.
HTTP API
All routes live under /api/marketing. The public ones serve visitor sites; the others need a DMS JWT.
| Route | Auth | Purpose |
|---|---|---|
POST /collect | public | Event ingestion |
GET /tracker.js | public | The tracker script |
GET /experiments.js?website= | public | Per-visitor A/B assignments, as a script |
GET/POST/PUT/DELETE /websites | tenant member / owner | Website CRUD |
GET /stats/overview, /stats/pages, /stats/heatmap | tenant member | Dashboard, page inventory and heatmap reads |
GET /funnels/:id/results | tenant member | Funnel and A/B computation |
GET/PUT /settings | platform owner | Runtime settings |
The complete route table, with the event, snapshot and assignment contracts, is in Architecture.