Noon Docs · Getting Started · Concepts · Guides · Reference

A form on a Noon page lets a visitor send you a message or sign up for a list, without any custom backend code. This guide walks through putting a working contact or signup form on a page, then reading what people submitted.

A page may hold at most one form. The block type is FormBlock.

Before you start: create a delivery connection

A form needs somewhere to send what it collects. Create at least one of these first, with manage_connection. You will put the connection's returned id into the form block in the next step.

Option A: an SMTP notification. Noon sends you one email per submission through your own mail account.

{
  "action": "create",
  "input": {
    "provider": "smtp",
    "label": "Contact form notifications",
    "credentials": {
      "host": "smtp.your-provider.example",
      "port": 587,
      "username": "[email protected]",
      "password": "the-mailbox-password"
    },
    "meta": {
      "from": "[email protected]",
      "fromName": "Your Site",
      "to": "[email protected]"
    }
  }
}

Required credentials for an smtp connection: host, username, password. port must be 587 (it defaults to 587 if you omit it; SMTP submission over STARTTLS is the only mode supported). Meta: from is required (the envelope sender); to is optional and defaults to from if you leave it out; fromName is an optional display name.

Option B: a Brevo list subscription. Noon adds the submitter's email to a Brevo list.

{
  "action": "create",
  "input": {
    "provider": "brevo",
    "label": "Newsletter list",
    "credentials": { "apiKey": "xkeysib-your-brevo-api-key" },
    "meta": { "listId": 3 }
  }
}

Required credentials for a brevo connection: apiKey. Meta: listId (the numeric Brevo list id), optional but a subscription with no listId does nothing.

Option C, optional: a Turnstile captcha. Adds a Cloudflare Turnstile challenge to the form.

{
  "action": "create",
  "input": {
    "provider": "turnstile",
    "label": "Contact form captcha",
    "credentials": { "secretKey": "your-turnstile-secret-key" },
    "meta": { "siteKey": "your-turnstile-site-key" }
  }
}

Required credentials for a turnstile connection: secretKey. Meta: siteKey (the public key the widget renders with).

Each call returns a connection object with an id field, for example cn_01j8x9k2.... That id is what you put into the form block.

Author the FormBlock

A FormBlock is one entry in a page's layout.blocks array. Here is a complete contact form, ready to pass to create_page or update_page:

{
  "type": "FormBlock",
  "props": {
    "fields": [
      { "name": "full_name", "label": "Your name", "type": "text", "required": true, "placeholder": "Jane Doe", "width": "50" },
      { "name": "email", "label": "Email address", "type": "email", "required": true, "placeholder": "[email protected]", "width": "50" },
      { "name": "message", "label": "Message", "type": "textarea", "required": true, "placeholder": "How can we help?" }
    ],
    "submit": "Send message",
    "success": "Thanks. We will reply within one business day.",
    "error": "Something went wrong. Please try again.",
    "mailerConnectionId": "cn_01j8x9k2smtp",
    "captchaConnectionId": "cn_01j8x9k2turnstile",
    "honeypot": "company_website"
  }
}

Placed in a page, that looks like:

{
  "slug": "contact",
  "title": "Contact us",
  "layout": {
    "blocks": [
      { "type": "FormBlock", "props": { "...": "as above" } }
    ]
  }
}

Top-level props

PropRequiredBounds
fieldsyesarray of 1 to 20 field objects
submityesbutton label text, 1 to 80 characters
successyesmessage shown after a successful submission, 1 to 300 characters
erroryesmessage shown after a rejected submission, 1 to 300 characters
honeypotyesa field name (see below) used as an anti-bot trap
espConnectionIdone of this or mailerConnectionId required1 to 64 characters
mailerConnectionIdone of this or espConnectionId required1 to 64 characters
captchaConnectionIdno1 to 64 characters

A form must name at least one of espConnectionId or mailerConnectionId. Naming both is allowed: the submission is then both emailed to you and added to the Brevo list. This describes a form with no actions array. When actions is present the two connection props are still required at write time, but they no longer deliver anything: actions takes delivery over entirely. Naming neither is rejected at write time.

Field bounds

Each entry in fields is:

Field keyNotes
namelowercase, must match ^[a-z][a-z0-9_]{0,31}$
label1 to 120 characters
typeone of text, email, textarea, checkbox, select
requiredboolean
placeholderoptional, only on text/email/textarea, 1 to 160 characters
optionsrequired on select (2 to 20 entries, each 1 to 100 characters), forbidden on every other type
widthoptional, one of 20, 25, 33, 50, 66, 75, 100 (percent); omit for full width

A form must declare exactly one field of type email, and that field must have required: true. Field names must be unique within the form. The honeypot name must not equal any field name, and neither a field name nor the honeypot may equal formId or cf-turnstile-response (both are reserved for the submission transport).

Publish the page

Create or update the page with create_page / update_page, then call publish on it. The form is inert on an unpublished draft or a preview: it renders with every control disabled until the page carrying it is live.

What happens when someone submits

Submissions post to POST /api/v1/forms/submit as a normal application/x-www-form-urlencoded form. The page itself needs no JavaScript to work: a small same-origin script (/noon-form.js) upgrades the submit into a background request so the visitor sees the authored success or error text without a page reload, but the plain HTML form still posts correctly with JavaScript turned off.

Three layers guard the endpoint before anything is stored:

  • Honeypot. The field named in honeypot renders off-screen and out of the tab order. A human never fills it in; a submission with a value in that field is rejected.
  • Rate limits, each on a rolling 10-minute window: 30 submissions per visitor IP across all forms, 10 per visitor IP for one form, and 300 total for one form.
  • Captcha, if you set captchaConnectionId. The widget must be solved before the submit button is enabled, and the token is verified server-side before anything is stored.

If a captcha is configured and fails, the submission is refused outright and nothing is stored. Otherwise the submission is stored first, and delivery is attempted after: an SMTP or Brevo failure never causes the visitor's submission to be lost, and the visitor still sees your authored success message.

Read what people submitted

Submissions are not visible anywhere in the page content. Read them with the query tool against the form_submissions collection, using a token with content:read (rows carry personal data, so query:read alone is not enough).

{
  "collection": "form_submissions",
  "select": ["id", "email", "fields", "espStatus", "mailStatus", "createdAt"],
  "where": { "formRef": "the-page-id-the-form-was-submitted-on" },
  "orderBy": [{ "field": "createdAt", "dir": "desc" }],
  "limit": 20
}

formRef is the id of the page the form lives on (the id returned by create_page), not its slug.

Queryable fields: id, formRef, email (the submitter's address), fields (a JSON object with every other typed field the visitor entered, keyed by field name), espStatus, mailStatus, espAttempts, espLastAttemptAt, createdAt, updatedAt. Only id and createdAt can be used to sort or page through results.

Two columns exist on the underlying table but can never be read back through query, under any select, filter, sort, or cursor: a hashed form of the email and a hashed form of the submitter's IP address, kept only for rate-limiting and deduplication.

A note on advanced routing

Beyond espConnectionId/mailerConnectionId/captchaConnectionId, a FormBlock also accepts an optional actions array (1 to 8 entries) that can send multiple emails, subscribe to an ESP, and call a webhook per submission, with a small closed template vocabulary for the email subject and body. This is a more advanced authoring surface than the guide above; for a straightforward contact or signup form, the three connection props are enough.