Pages

A page is a schema — it defines what information gets collected. Each submission creates a Record that stores the field values, timestamps, and any attached files.

Pages vs. Records

Think of a page as a database table definition: it describes the shape of your data. Records are the rows — individual filled-in submissions of that page.

One page can have thousands of Records. Each Record stores its field values, a creation timestamp, an optional submitter email, and a full audit trail of changes.

Creating a page

In the sidebar, click Pages then New Page. Building pages — defining fields, webhooks, and everything else in this guide — is an Admin/Owner action; Editors and Viewers don't see the Pages section at all, only the records that come through it. You'll provide:

The page's Kanban tab can also set a Default Board (and optionally a Default Column) — new records then land on that board automatically as cards. If no column is chosen, the board's first column is used.

Field types

Add fields via the Add Field button on the page editor. Each field has a type, label, placeholder, and optional flags: required, show on board card (the value appears on the record's Kanban card), and internal (hidden from the public page — visible to your team only).

TypeWhat it rendersStored as
textSingle-line text inputString
textareaMulti-line text areaString
emailEmail input with format validationString
numberNumeric inputNumber
telPhone number inputString
selectDropdown with your defined optionsString (chosen option)
radioRadio button group with your defined optionsString (chosen option)
checkboxSingle true/false toggleBoolean
dateDate pickerISO date string
sequenceAuto-assigned document number (e.g. PO-0042) drawn from an org-wide sequence. Read-only unless "allow manual editing" is enabled — see Auto-numberingString (assigned server-side on create)
fileFile upload buttonFile reference (stored in Snapi)
hiddenHidden input — not shown to the submitterString
objectGroup of nested sub-fields (text, number, dropdown, radio, date, checkbox, reference …)Object
arrayRepeating rows of sub-fields with add/remove and optional drag-reorder — rows can include reference pickers (e.g. PO lines, each choosing a Part)Array
tableRead-only data grid, with optional row actions: navigate to a page carrying {{row.x}} params (list → detail) or call a webhook with the row. Its rows come from either a data source — pick a different page in the builder and this block reads that page's records directly, no webhook needed — or, if no data source is set, the page's own onload webhook response via a data path (legacy)Not stored (display)
chartBar, line, pie, or donut chart. Rows come from the same two sources a table field can use — a picked data source, or the onload webhook via a data path — grouped by a chosen field and reduced with Sum, Count, Average, Min, or Max, computed entirely client-side, no aggregation webhook requiredNot stored (display)
referenceSearchable dropdown linking one record from another page (with inline "create new" inside Snapi). Works at the top level, inside object fields, and in array rows. Top-level reference fields can now be shown on the public page — uncheck Internal and visitors get a live search-as-you-type picker against the referenced page's records (scoped, short-lived access — no admin credential is ever exposed). Reference fields nested inside object/array sub-fields stay internal-only for nowRecord ID + title label
displayRead-only text shown on the pageNot stored
titleLarge page heading (layout only)Not stored
subtitleSection heading (layout only)Not stored
dividerHorizontal divider (layout only)Not stored
linebreakLine break (layout only)Not stored
buttonAction button: call a webhook URL (server-side proxied), navigate to another page (with params), or go back — see NavigationNot stored
linkText link: navigate to another page (with params), open an external URL (interpolated), or go backNot stored

Tip: Field order matters — drag fields to reorder them (sub-fields of object/array fields reorder the same way inside the field editor). The order in the editor is the order in the public page.

Embedded pages can link to each other, turning a set of pages into a lightweight app on any website — all inside one embed container, SPA-style, with browser back/forward support and no page reloads.

MechanismWhat it does
Navigate button / linkA button or link field with the Navigate to another page action loads the target page in place. Optional params carry data across: values support {{data.fieldName}} (current inputs) and {{params.x}}
Table row actionsPer-row buttons on a data table: navigate to a page with {{row.x}} params from the clicked row (list → detail), or call a webhook with the row payload
Back buttonA button field with the Go back action returns to the previously shown page (browser history)
Post-submit navigationPer page (Settings → "After submit, navigate to"): after a successful submit the embed loads the target page. Params support {{data.fieldName}} and {{record.id}} (the new record)
Receiving paramsThe target page reads params as {{params.x}} — in display fields, or as a field's default value to prefill it. URL query params (?x=y) work the same way
SDKSnapi.navigateToForm(slug, params) is also callable directly from your page's JavaScript
Board header navigationAdmins can add link/button items to a kanban board's header too (same editor, reduced to those two types), so users can move between boards and pages freely — board targets work inside Snapi and are hidden on public embeds, form targets work everywhere

Passing data around: {{data.x}}, {{params.x}}, {{row.x}}, {{record.id}}

Several places in Snapi accept small template placeholders that are replaced with live values. There are four sources, and where you are determines which ones exist:

PlaceholderWhere the value comes fromAvailable in
{{data.fieldName}}The current page: what the user has typed so far, merged over any onload-webhook data. Use dot-paths for nested values ({{data.customer.email}})Display fields, navigation params, external link URLs, button payloads
{{params.x}}How the visitor arrived: URL query parameters (?x=y) merged with params passed by whichever navigation brought them here. Navigation params winDisplay fields, field default values (prefill!), hidden fields, nav params
{{row.x}}The clicked table row — only meaningful inside a table's row-action paramsRow-action params
{{record.id}}The id of the record that was just createdPost-submit navigation params only

The pattern that ties an app together: page A sends a value as a nav param → page B receives it as {{params.x}} → B surfaces it via a display field, prefills an input with it (set the field's default value to {{params.x}}), or carries it invisibly in a hidden field — which submits with the record, so your webhooks see where the user came from.

Worked example — order follow-up: the Orders page's post-submit navigation targets "Order Feedback" with params {"order": "{{record.id}}", "email": "{{data.email}}"}. Order Feedback has a hidden field order_ref with default {{params.order}} and an email field with default {{params.email}} (prefilled, editable). When feedback is submitted, the record — and your n8n webhook — carries order_ref linking it back to the order.

Unknown placeholders resolve to an empty string — they never error. Values are substituted at render/click time with whatever is currently in the page.

External users (sign-in-required pages)

Snapi distinguishes two kinds of people. Internal users are your team on the inside — they sign in to the Snapi workspace, each with a role (viewer / editor / admin / owner). Every role can work the data (view and fill in records, move cards on a board); building pages and boards themselves — the schema, the webhooks, the columns — is Admin/Owner only. External users are your application's end users — the clients, vendors, and field staff who use what you build: they sign in on your embedded pages, portals, and apps, and never see the Snapi workspace at all.

Turn on Require sign-in for a public page and only authenticated users can view or submit it: internal users always can; external users can when assigned (per page, or all gated pages). External users are managed in Settings → External Users (a Team plan feature), can be suspended instantly, and share the plan's seat pool with internal users. The embedded page shows a sign-in screen automatically; sessions last 7 days.

Matching your brand: themes and accent colors

There are two levels of styling, and most people only need the first:

Either way, no code is required — theming is applied automatically wherever the page renders: the in-app builder, an embedded page, or a standalone hosted page. If you need to go further than the color pickers — CSS custom properties, or restyling specific field/button classes directly from your own site's stylesheet — see the full reference in Styling the embedded page.

Webhook runs & proxying

All webhook calls — pre/post submit, onload data fetches, and button/row actions — are made server-side: no CORS requirements on your webhook endpoints, and headers/secrets never reach the browser. Every call is logged with status, duration, and error detail; see the Recent Webhook Runs panel on the page builder's Webhooks tab (last 100 calls per page).

Auto-numbering with sequences

Sequences give records automatic document numbers — PO numbers, invoice numbers, ticket IDs. A sequence is org-wide and named, so several pages can share one counter. Manage them in Settings → Sequences (or create one inline from the field editor), then add a Sequence Number field to a page and pick the sequence.

SettingWhat it does
PrefixText before the number, e.g. PO-. Supports date tokens {YYYY} {YY} {MM} {DD} rendered at assignment time — e.g. INV-{YYYY}-INV-2026-0042
Zero-paddingDigit width of the number: padding 4 → 0042
Counter resetOptionally restart at 1 every year or month. Pair with a matching date token in the prefix, or numbers will repeat across periods
Allow manual editingPer field: users may type their own value; leaving it empty still auto-assigns. Manual values never move the counter

Numbers are assigned server-side, atomically with the record insert — concurrent submissions (UI, public pages, REST API, and MCP agents alike) can never receive the same number. Values are stored as plain text and frozen at creation: renaming the sequence or changing its prefix later never rewrites existing records. When the sequence field is a page's first text-like field, its value becomes the record's title automatically.

Public page URL

Every page has a shareable public URL in the format:

https://app.snapi.ca/embed/{org-slug}/{page-slug}

Anyone with this URL can submit the page — no account needed. Use this link in emails, on websites, or in chatbots. To embed the page in an iframe or use the JavaScript SDK, see Embedding Pages.

Viewing and managing records

The Records view on any page shows all submissions in reverse chronological order. Each row shows the submission time, the value of the first text-like field as a title, and any status or board assignment.

Clicking a record opens the detail panel, where you can:

Webhooks on submit

Each page can have webhooks that fire around the page lifecycle. There are three types:

TypeWhen it firesCan abort the submission?
onload webhookWhen the page loadsNo — its response can prefill page data before anything is submitted
pre-webhookBefore the record is savedYes — respond with {"abort": true} to reject the submission and surface the error to the submitter; the response can also modify the submitted data before it's saved
post-webhookAfter the record is savedNo — the record already exists; use for notifications and downstream automation

The pre-webhook payload is a JSON object containing the page's ID, name, and slug plus all submitted field values; post-webhook payloads also include the saved record. See Webhooks for the full payload schema and authentication headers.

Note: pages are called entities in the API for backwards compatibility — API paths and webhook payload keys use entity.

Record data via API

You can read and create records programmatically using your API key. This is useful for building integrations, bulk imports, or giving an AI agent read access to submitted data. See API Keys for the endpoints and authentication details.

Note on file uploads: File fields store a reference to the uploaded file, not the raw bytes. Use the GET /api/v1/files/:filename endpoint to download the actual file. Files count against your plan's storage limit.