Skip to main content
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

  1. Create a page (e.g. /weight/checkin)
  2. Add a <script scope="backend"> that checks for POST, validates request.body with the Schema validator, and stores the data
  3. Your frontend uses fetch() to POST JSON data to that page
  4. 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

FunctionDescription
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

PropertyAllowed types / notes
slugentity slug string — required; must exist for the brand
referenceTypestring (alphanumeric, -, _) or omit / null
referenceIdstring (max 512 chars) or omit / null
fieldKeystring (alphanumeric, _, ., -) — required
valuestring, number, boolean, or JSON object — required for set (0 / false valid)

Input validation

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.
  1. Validate with Schema — use the built-in Schema validator to enforce types, lengths, and allowed values
  2. Check authentication — verify is_customer before storing data
  3. Use response.json() — this stops page rendering and returns JSON directly
  4. Use response.status() — set proper HTTP status codes (400 for validation errors, 401 for auth)
  5. Share validation logic — create a backend script module for reusable schemas and auth checks

POST body limits

ConstraintValue
Max body size64 KB (larger payloads are dropped)
Content typeapplication/json only
Body available asrequest.body (parsed object or null)