Skip to content
Elara One docs

Lead forms

Render a store form from its schema, submit it safely with spam protection and consent, and know what happens to each lead.

Forms are built in the store admin (enquiry, quote request, newsletter sign-up, or blank) and published to your storefront through the API. Your site reads the form's schema, renders it in your own design, and posts the answers back. Each submission lands in the CRM as a contact, and can open a lead, send email notifications and start flows.

Read the form

GET /v1/forms/{handle} returns the public schema. It never includes the form's notification settings or internal wiring.

curl https://api.elaraone.app/api/v1/forms/quote-request \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET"
Response (trimmed)
{
  "data": {
    "handle": "quote-request",
    "name": "Quote request",
    "fields": [
      {
        "handle": "name",
        "type": "text",
        "label": "Your name",
        "required": true,
        "options": [],
        "rules": ["max:120"],
        "constraints": { "max_length": 120 },
        "marketing_consent": false
      },
      {
        "handle": "email",
        "type": "email",
        "label": "Email",
        "required": true,
        "options": [],
        "rules": [],
        "constraints": {},
        "marketing_consent": false
      },
      {
        "handle": "budget",
        "type": "select",
        "label": "Budget",
        "required": false,
        "options": ["Under £500", "£500 to £2,000", "Over £2,000"],
        "rules": [],
        "constraints": {},
        "marketing_consent": false
      },
      {
        "handle": "news",
        "type": "checkbox",
        "label": "Send me offers and news from Hearth & Oak",
        "required": false,
        "options": [],
        "rules": [],
        "constraints": {},
        "marketing_consent": true
      }
    ],
    "captcha": { "provider": "turnstile", "site_key": "0x4AAAAAAA…" }
  }
}

A form that does not exist or is switched off answers 404 NOT_FOUND; the API never says which.

Render the fields

typeRender asSent as
text<input type="text">string
email<input type="email">string, checked as an email address
phone<input type="tel">string
textarea<textarea>string
select<select> with optionsone of options, exactly
checkbox<input type="checkbox">, untickedtrue or false
hidden<input type="hidden"> that your page fills in (a product name, for example)string
number<input type="number">number
date<input type="date">a date string such as 2026-11-14

File upload questions are coming soon. Until then the API leaves them out of the schema.

Use each field's handle as the input name and label as its label. For client-side checks:

  • required on a checkbox means it must be ticked.
  • constraints maps the server's rules onto HTML attributes: min_length and max_length on text-like fields, min and max on numbers. Use them for minlength, maxlength, min and max.
  • rules lists the exact rules the server applies, such as max:120 or alpha_dash, if you want to mirror more of them.

The server checks everything again, so client-side checks are for the shopper's convenience only.

Spam protection

Every form has two protections. Render both.

Honeypot. Add a text input named _hp, hidden from people (off-screen with CSS, tabindex="-1", autocomplete="off") and always sent empty. A bot that fills it gets a normal-looking 202 and nothing is stored or sent.

Cloudflare Turnstile. When captcha is not null, render the Turnstile widget with captcha.site_key and send its token as _captcha. The token is checked against the store's registered websites. When captcha is null, the store's forms are not captcha-protected and you send no _captcha.

<div class="cf-turnstile" data-sitekey="0x4AAAAAAA…"></div>
<input type="text" name="_hp" value="" tabindex="-1" autocomplete="off" class="visually-hidden" aria-hidden="true">

The Turnstile widget puts its token in a field named cf-turnstile-response. Copy that value into _captcha when you build the request.

Submit the answers

Post the answers keyed by field handle, plus the protocol fields:

FieldWhat it is
_hpThe honeypot. Always empty.
_captchaThe Turnstile token, when the form has a captcha.
_pageThe page the form was on, as a path or URL. Only the path is kept.
utm_source, utm_medium, utm_campaign, …Campaign values from the landing page, if you kept them.

Send an Idempotency-Key so a double click or a retried request records one submission. If the shopper has a basket, send its X-Cart-Token too: the lead then picks up the basket's campaign source.

curl -X POST https://api.elaraone.app/api/v1/forms/quote-request \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Client-IP: 203.0.113.7" \
  -H "Idempotency-Key: 4b7f0c1e-2d3a-4e5f-8a9b-0c1d2e3f4a5b" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Katherine Johnson",
    "email": "[email protected]",
    "budget": "£500 to £2,000",
    "news": true,
    "_hp": "",
    "_captcha": "0.Zx…",
    "_page": "/quotes/kitchen-tables",
    "utm_source": "newsletter",
    "utm_medium": "email",
    "utm_campaign": "autumn"
  }'

A successful submission answers 202:

{ "data": { "result_id": 5120 } }
StatusCodeWhen
422VALIDATION_FAILEDA field failed its rules. details is keyed by field handle. A failed captcha is reported on _captcha.
503CAPTCHA_UNAVAILABLETurnstile gave no answer. Nothing was stored. Ask the shopper to try again shortly.
404NOT_FOUNDThe form does not exist or is switched off.
429RATE_LIMITEDMore than 30 submissions a minute from one visitor.

A checkbox with marketing_consent: true is the form's marketing opt-in. Render it unticked, separate from any terms box. Whatever the shopper chooses is recorded on the contact as proof of consent, and marketing messages sent from the CRM check it before they go.

If someone who earlier asked the store to erase their data ticks the box again, the engine asks them to confirm before lifting the block:

  • By email: they get a link to the store's marketing consent confirmation page, with ?token=…. That page shows a Confirm button which posts { "token": "…" } to POST /v1/marketing/confirm. Post on the button press, never on page load.
  • By text: if they gave a phone number, they get a 6-digit code. After any consenting submission with a phone number, offer an "Enter the code we texted you" step that posts { "phone": "…", "code": "123456" } to the same route.

Show the code step to everyone who consented with a phone number: the form's answer never says whether a code was sent. Success is { "data": { "status": "confirmed" } }; anything else is 422 CONSENT_CONFIRMATION_INVALID. Email links last 24 hours and codes 15 minutes.

What happens next

When a submission is accepted:

  1. A contact is created or updated in the CRM, with the page and campaign it came from.
  2. A lead opens for a new contact, or for a known contact with no open lead, and the store's lead routing rules assign it. Newsletter sign-up forms are the exception: they create subscribers, not leads.
  3. Email notifications go out, up to 10 per form, each with its own recipients (the store admin, the person who filled it in, the lead's owner, chosen staff or other addresses), subject, message and choice of fields.
  4. Flows with the Form submitted trigger start, filtered by form if they ask. See Flows.

A honeypot hit or a failed captcha does none of these.