Embedding pages
One snippet turns any static page into structured intake: your marketing site, a client's WordPress blog, a Notion-published page, the plain HTML brochure site an agency shipped in 2019. Visitors see a page; you see records flowing onto a board, triggering webhooks, ready for your team or an AI agent — with zero backend on the embedding site.
How it works
Each page in Snapi has a public submission endpoint:
POST https://api.snapi.ca/api/v1/public/submit/:entityId
The embed script renders your page automatically from its field schema and posts to this endpoint. Records appear in your workspace in real time.
Getting the embed snippet
- Open your workspace and navigate to Pages.
- Click the page you want to embed.
- Click the Share / Embed button in the top-right toolbar.
- Copy the snippet from the Embed tab.
It's a self-sizing iframe — no script tag with data attributes, no build step:
<iframe
id="snapi-embed-..."
src="https://app.snapi.ca/embed/your-org/contact-form"
width="100%"
height="480"
frameborder="0"
scrolling="no"
style="border: none; border-radius: 8px;"
title="Contact Form"
></iframe>
<script>
// Listens for the embedded page reporting its own content height,
// so the iframe grows/shrinks with the form and never shows its
// own scrollbar. Included automatically when you copy the snippet.
</script>
No website? Use the standalone hosted page
Every public page also has its own real, shareable web page — not an iframe, not a snippet, just a link:
https://app.snapi.ca/forms/your-org/contact-form
It's a full page with your org name, the page's icon/color/description, and the page itself — styled the same way as an embed (see Styling below). Unlike the iframe embed URL, it's indexable and carries Open Graph tags for link previews, so it's meant to be shared directly: an "Apply Now" button in a job listing, a link in an email, a QR code on a poster. Get the link from the page's Share / Embed panel, or replace /embed/ with /forms/ in an iframe URL you already have.
Restricting where your page can be used
Set Allowed Domains in the page's Public Page tab (e.g. example.com, another.com) to control where the page works. Enforcement happens in two places:
- When the embed page loads — an iframe on a site that isn't on your list gets "This form cannot be embedded on this site" instead of the page.
- When a submission arrives — script-embed submissions from a non-allowed origin are rejected with
403.
Subdomains are included automatically (example.com also allows www.example.com). Leave the field empty to allow embedding anywhere.
Gotchas
- "app.snapi.ca refused to connect" — the page isn't public (enable the Public Page toggle and save), or the iframe URL wasn't copied from the Embed Code box.
- Don't add
app.snapi.cato your Allowed Domains. Submissions from an iframe embed technically arrive from Snapi's own origin — that's normal browser behavior and handled automatically. Your list should contain only the site(s) that embed the page. - Domain restriction is a strong deterrent, not DRM. Browsers verify the embedding site via standard headers, which a non-browser client can spoof. For sensitive flows, enable Require Authentication or validate submissions in a pre-submit webhook.
- Internal fields never appear on public pages — fields marked Internal are stripped from the public page config and from any submission that tries to set them.
- Script updates:
snapi.jsis cached for up to an hour — changes to the SDK can take that long to reach embedded pages.
Styling the embedded page
snapi.js renders plain, real DOM into your page — no shadow root, no build step. That cuts both ways: your site's CSS can't accidentally clash with it (everything is scoped under the .snapi-form class), but it also means your CSS can reach in and restyle anything, as long as it loads after snapi.js or uses higher specificity.
Colors: the six CSS custom properties
Every color in the default styles reads from a CSS custom property with a hardcoded fallback, so overriding six variables reskins the whole page — inputs, buttons, links, focus rings, everything:
:root {
--snapi-primary: #2551ed; /* buttons, links, focus ring, submit */
--snapi-text: #111827; /* labels, title, radio text */
--snapi-bg: #ffffff; /* form background */
--snapi-border: #d1d5db; /* input borders, table header rule */
--snapi-danger: #ef4444; /* danger buttons, retry button */
--snapi-success: #10b981; /* success icon */
}
snapi.js injects its own <style> block defining these same six variables at :root — added to the page after your own stylesheet, so it wins. Either style purely through the Theme picker, or leave the page's Theme unset and drive colors entirely from your own CSS.Everything else: the full class reference
For layout, spacing, and anything the six color variables don't cover, target these classes directly. Every field type, button, and state snapi.js can render has one.
Structure & typography
| Class | What it is |
|---|---|
.snapi-form | The root container — font, base text color, padding |
.snapi-title / .snapi-subtitle | Auto title (or a Title/Subtitle field's rendered text) |
.snapi-fields, .snapi-cols-1/2/3 | The field grid and its column-count variants (stack to 1 column under 640px) |
.snapi-field | One field's wrapper (label + input); .snapi-field label for the label text |
.snapi-required | The red * on required fields |
.snapi-hr, .snapi-field-divider | A Divider field |
.snapi-field-display, .snapi-display-val, .snapi-display-title | Display fields (read-only {{data.x}} text) |
.snapi-link-bar, .snapi-link | Link fields — rendered as a nav-style row at the top |
.snapi-actions | Bottom action row holding custom Buttons + the Submit button |
Inputs & buttons
| Class | What it is |
|---|---|
.snapi-field input, textarea, select | Text/number/email/tel/date/select/textarea — one shared rule, plus a :focus variant |
.snapi-radio-label | One radio option's label row |
.snapi-seq-auto | A Sequence field left to auto-assign (greyed, italic, not editable) |
.snapi-submit | The Submit button |
.snapi-btn, .snapi-btn-primary/secondary/danger, .snapi-btn-sm | Button fields and table row-action buttons, by style variant and size |
.snapi-field-button | A standalone Button field's wrapper |
File & reference fields
| Class | What it is |
|---|---|
.snapi-file-zone | The drag-and-drop file dropzone |
.snapi-drag-over, .snapi-file-selected | Dropzone states — a file is being dragged over it / one's chosen |
.snapi-file-icon | The icon inside the dropzone |
.snapi-ref-wrap, .snapi-ref-search | A Reference field's search input |
.snapi-ref-dropdown, .snapi-ref-open | Its results dropdown, and the open-state modifier |
.snapi-ref-option, .snapi-ref-empty | One result row, and the "no matches" row |
Tables & charts
| Class | What it is |
|---|---|
.snapi-table-scroll, .snapi-table | A Table field — horizontal-scroll wrapper and the table itself |
.snapi-table-actions | A row's action-button cell |
.snapi-table-empty | "No rows" state |
.snapi-chart-wrap | A Chart field's canvas container |
Messages, states & the auth modal
| Class | What it is |
|---|---|
.snapi-loading, .snapi-spinner | Initial load state |
.snapi-error, .snapi-btn-retry | Page failed to load, with its Try Again button |
.snapi-success, .snapi-success-icon | Post-submission success screen |
.snapi-form-error | Inline submit-failure message |
.snapi-user-bar, .snapi-user-logout | "Signed in as…" bar on auth-required pages |
.snapi-login-prompt, .snapi-login-form | The sign-in screen shown before an auth-required page |
.snapi-modal, .snapi-modal-open, .snapi-modal-backdrop, .snapi-modal-box, .snapi-modal-close, .snapi-modal-title | The login modal and confirm-action dialogs |
.snapi-confirm-box, .snapi-confirm-msg, .snapi-confirm-actions | A button/row action's confirmation prompt |
.snapi-toast, .snapi-toast-in, .snapi-toast-success/error/info | Bottom-right toast notifications |
.snapi-powered | The "Powered by Snapi" footer |
snapi.js update could rename or restructure one. If you're overriding more than colors, keep your CSS scoped (e.g. under a wrapper id) so a future change fails loudly instead of silently.Submission limits
Public page submissions are unlimited on every plan, including Free — no monthly quota, no record cap. See Plans & limits for the full comparison.
Spam and rate limiting
The public submit endpoint is IP-rate-limited (60 requests per 15 minutes per IP) regardless of plan. For high-volume use cases, authenticated API access via an API key is recommended instead.
Receiving submissions in real time
Every submission triggers your page's configured webhooks (Builder plan or higher). You can use a post-submit webhook to send confirmation emails, sync to a CRM, or trigger automation in n8n.