Pages accept both GET and POST requests. Combined with backend scripts, this lets you build data-collection endpoints, custom forms, and mini-apps using regular ElasticFunnels pages — no external server needed.
The pattern
- Create a page (e.g.
/weight/checkin)
- Add a
<script scope="backend"> that checks for POST, validates request.body with the Schema validator, and stores the data
- Your frontend uses
fetch() to POST JSON data to that page
- The backend script responds with JSON — the page HTML is skipped when
response.json() is called
For reusable validation logic across multiple pages, use shared backend script modules — create a module with exported functions and import them anywhere.
Full example: Weight check-in
1. Create a shared validation module
Create a backend script module (code: validators) with shared schemas:
<script scope="backend">
export var WeightCheckinSchema = Schema.object({
weight: Schema.number().required().min(50).max(500),
unit: Schema.string().valid('lbs', 'kg').default('lbs'),
notes: Schema.string().max(500).default(''),
});
export function requireCustomer() {
if (!is_customer) {
response.status(401);
response.json({ ok: false, error: 'not authenticated' });
}
}
</script>
2. Create the page /weight/checkin
<script scope="backend">
import { WeightCheckinSchema, requireCustomer } from "validators";
if (request.method === 'POST') {
requireCustomer();
var result = WeightCheckinSchema.validate(request.body);
if (!result.valid) {
response.status(400);
response.json({ ok: false, errors: result.errors });
}
var data = result.value;
var entitySlug = 'pattern-tracker'; // your CRM entity slug from settings
setCrmField({ slug: entitySlug, fieldKey: 'current_weight', value: data.weight });
setCrmField({ slug: entitySlug, fieldKey: 'weight_unit', value: data.unit });
setCrmField({ slug: entitySlug, fieldKey: 'last_checkin', value: new Date().toISOString() });
if (data.notes) {
setCrmField({ slug: entitySlug, fieldKey: 'checkin_notes', value: data.notes });
}
response.json({ ok: true, data: data });
}
setVariable('page_title', 'Weight Check-in');
</script>
<div>
<h1>{{ var.page_title }}</h1>
<form id="checkin-form">
<input type="number" id="weight" placeholder="Current weight" step="0.1" required />
<select id="unit">
<option value="lbs">lbs</option>
<option value="kg">kg</option>
</select>
<textarea id="notes" placeholder="Notes (optional)"></textarea>
<button type="submit">Log Weight</button>
</form>
<div id="result"></div>
</div>
<script>
document.getElementById('checkin-form').addEventListener('submit', async function(e) {
e.preventDefault();
var res = await fetch('/weight/checkin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
weight: parseFloat(document.getElementById('weight').value),
unit: document.getElementById('unit').value,
notes: document.getElementById('notes').value
})
});
var data = await res.json();
document.getElementById('result').textContent = data.ok
? 'Logged: ' + data.data.weight + ' ' + data.data.unit
: 'Error: ' + (data.errors || [data.error]).join(', ');
});
</script>
3. Read the data back on another page
On any page (e.g. /dashboard), read the CRM data:
<script scope="backend">
import { requireCustomer } from "validators";
requireCustomer();
var entitySlug = 'pattern-tracker';
var weight = getCrmField({ slug: entitySlug, fieldKey: 'current_weight' });
var goal = getCrmField({ slug: entitySlug, fieldKey: 'goal_weight' });
setVariable('current_weight', weight || 'Not logged yet');
setVariable('goal_weight', goal || 'Not set');
setVariable('on_track', weight && goal && Number(weight) <= Number(goal));
</script>
<h2>Current weight: {{ var.current_weight }}</h2>
<h2>Goal: {{ var.goal_weight }}</h2>
<p v-if="var.on_track">You've reached your goal!</p>
How it fits together
Frontend (browser) Backend (sandbox)
───────────────── ─────────────────
fetch('/weight/checkin', { → request.method = 'POST'
method: 'POST', request.body = { weight: 182, ... }
body: JSON.stringify(data)
}) → Schema.validate(request.body)
→ setCrmField({ slug: 'pattern-tracker', fieldKey: 'current_weight', value: 182 })
← { ok: true, data: {...} } ← response.json({ ok: true, data })
CRM functions reference
| Function | Description |
|---|
getCrmEntries({ slug, ... }) | Fetch entries — slug required (Data Functions) |
getCrmField({ slug, fieldKey, ... }) | Get one field |
setCrmField({ slug, fieldKey, value, ... }) | Set one field |
slug (CRM entity) vs reference
slug — required on every call. This is the CRM entity slug from entity settings (e.g. pattern-tracker). It is not the same as referenceType (customer, order, …).
referenceType / referenceId — which record entries attach to. Omit or null for 'customer' and the session customer_id.
setCrmField({ slug: 'pattern-tracker', fieldKey: 'score', value: 85 });
setCrmField({
slug: 'pattern-tracker',
referenceType: 'order',
referenceId: '12345',
fieldKey: 'followup',
value: 'done',
});
Calls must use a single options object (no positional lists).
Parameter types
| Property | Allowed types / notes |
|---|
slug | entity slug string — required; must exist for the brand |
referenceType | string (alphanumeric, -, _) or omit / null |
referenceId | string (max 512 chars) or omit / null |
fieldKey | string (alphanumeric, _, ., -) — required |
value | string, number, boolean, or JSON object — required for set (0 / false valid) |
All inputs are strictly validated before reaching Elasticsearch:
slug is validated and resolved in MySQL; unknown slug → TypeError
fieldKey must match /^[a-zA-Z0-9_.-]+$/
referenceType must match /^[a-zA-Z0-9_-]+$/
referenceId rejects {}[]"\ characters
- String values are capped at 10,000 characters
- Numbers must be finite (no
NaN, Infinity)
- Objects are JSON-serialized and capped at 10 KB
- Missing
slug, fieldKey, or value (set) throws TypeError
- Other invalid inputs still cause
false / null / [] where applicable
Security checklist
Always validate request.body in your backend script. The body comes from the visitor’s browser and can contain anything.
- Validate with Schema — use the built-in Schema validator to enforce types, lengths, and allowed values
- Check authentication — verify
is_customer before storing data
- Use
response.json() — this stops page rendering and returns JSON directly
- Use
response.status() — set proper HTTP status codes (400 for validation errors, 401 for auth)
- Share validation logic — create a backend script module for reusable schemas and auth checks
POST body limits
| Constraint | Value |
|---|
| Max body size | 64 KB (larger payloads are dropped) |
| Content type | application/json only |
| Body available as | request.body (parsed object or null) |