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:
- Name — displayed on the public page header and in the Snapi UI (e.g., "Bug Report", "Intake Form").
- Description — optional subtitle shown under the page title.
- Success message — what the submitter sees after a successful submission.
- Visibility — Public (anyone with the URL can submit) or Private (requires a Snapi login).
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).
| Type | What it renders | Stored as |
|---|---|---|
| text | Single-line text input | String |
| textarea | Multi-line text area | String |
| Email input with format validation | String | |
| number | Numeric input | Number |
| tel | Phone number input | String |
| select | Dropdown with your defined options | String (chosen option) |
| radio | Radio button group with your defined options | String (chosen option) |
| checkbox | Single true/false toggle | Boolean |
| date | Date picker | ISO date string |
| sequence | Auto-assigned document number (e.g. PO-0042) drawn from an org-wide sequence. Read-only unless "allow manual editing" is enabled — see Auto-numbering | String (assigned server-side on create) |
| file | File upload button | File reference (stored in Snapi) |
| hidden | Hidden input — not shown to the submitter | String |
| object | Group of nested sub-fields (text, number, dropdown, radio, date, checkbox, reference …) | Object |
| array | Repeating 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 |
| table | Read-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) |
| chart | Bar, 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 required | Not stored (display) |
| reference | Searchable 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 now | Record ID + title label |
| display | Read-only text shown on the page | Not stored |
| title | Large page heading (layout only) | Not stored |
| subtitle | Section heading (layout only) | Not stored |
| divider | Horizontal divider (layout only) | Not stored |
| linebreak | Line break (layout only) | Not stored |
| button | Action button: call a webhook URL (server-side proxied), navigate to another page (with params), or go back — see Navigation | Not stored |
| link | Text link: navigate to another page (with params), open an external URL (interpolated), or go back | Not 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.
Page-to-page navigation (multi-page apps)
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.
| Mechanism | What it does |
|---|---|
| Navigate button / link | A 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 actions | Per-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 button | A button field with the Go back action returns to the previously shown page (browser history) |
| Post-submit navigation | Per 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 params | The 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 |
| SDK | Snapi.navigateToForm(slug, params) is also callable directly from your page's JavaScript |
| Board header navigation | Admins 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:
| Placeholder | Where the value comes from | Available 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 win | Display fields, field default values (prefill!), hidden fields, nav params |
{{row.x}} | The clicked table row — only meaningful inside a table's row-action params | Row-action params |
{{record.id}} | The id of the record that was just created | Post-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:
- Per-page accent color. Every page has a Color setting in the page builder (defaults to Snapi blue) — it's used for the page's icon and its submit button/field-focus accent wherever it renders: the builder, the hub, and every embedded/standalone instance of that page. No separate setup required.
- Reusable theme presets. For full control (background, text, borders, danger/success colors — not just the accent), go to Settings → Themes and create a named preset. Set one as your org default, or assign different presets per page in the page builder's Theme field — this is the one to reach for when you embed the same kind of page on multiple clients' sites and need each to match that client's palette.
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.
| Setting | What it does |
|---|---|
| Prefix | Text 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-padding | Digit width of the number: padding 4 → 0042 |
| Counter reset | Optionally restart at 1 every year or month. Pair with a matching date token in the prefix, or numbers will repeat across periods |
| Allow manual editing | Per 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:
- View all field values
- Edit any field value (as a Snapi team member)
- Add internal comments (not visible to the submitter)
- Attach files
- View the audit log — every field change, comment, and file with the user who made it and when
Webhooks on submit
Each page can have webhooks that fire around the page lifecycle. There are three types:
| Type | When it fires | Can abort the submission? |
|---|---|---|
| onload webhook | When the page loads | No — its response can prefill page data before anything is submitted |
| pre-webhook | Before the record is saved | Yes — 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-webhook | After the record is saved | No — 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.