Forms Overview
This page covers the ways to incorporate a form on an externally-hosted site
— your own site, your framework, your hosting — and connect it to
Business-in-a-Box through the @biab-dev/sdk.
This section is about forms on your own, externally-hosted site, connected through the SDK. It does not cover forms added to a site built with BIAB's internal Site Builder — those are placed and managed inside the builder itself.
In every case the form is created first in your BIAB dashboard, under Site Content → Forms — the single source of truth for its fields, validation, and the schema every submission is checked against. The builder is fast and deeply customizable, so even complex, multi-step forms come together quickly and most forms can live entirely in the dashboard. Only if the builder doesn't fit your needs do you drop down to a custom build. What changes between approaches is how much of the form you build yourself — and every approach falls into one of two categories.
Make the form "Live" first
For the SDK to fetch a form's schema or accept a submission, the form must be Live — the master on/off switch in the form's settings. Live is its own axis, independent of two other toggles:
- Chatbot access governs only whether BIAB's AI chatbot may surface the form inside a conversation. A form does not need chatbot access to be embedded or submitted through the SDK — being Live is enough.
- Public link publishes the Live form to a BIAB-hosted, shareable page at
{orgSlug}.<platform>/f/{formSlug}— handy when you just want a link to share and don't want to host anything yourself.
If a forms.schema(slug) / forms.submit(slug, …) call comes back
not_found, the most common cause is simply that the form isn't Live yet.
(In the chat flow, getChatForm / submitChatForm additionally require the
form to have Chatbot access on.)
1. You build the form — the SDK keeps it schema-correct
The form is written entirely on your own site (your markup, your styling), and the SDK guarantees it stays in lockstep with the schema BIAB defined.
- The form is defined in the dashboard, which holds every valid schema structure — each field, its type, whether it's required, and its output key.
- You confirm the schema typing through the SDK:
client.forms.schema(slug)returns the full, typed definition, so you can build (or adapt an existing) hand-written form and be sure it matches the receiving schema in BIAB. - You submit to your dashboard through the SDK, and validate first so a mismatch never leaves the browser.
import { createBiabDevClient, validateFormSubmission } from "@biab-dev/sdk";
const biab = createBiabDevClient({ apiKey, baseUrl });
// 1. Pull the schema BIAB will validate against — your contract.
const schema = await biab.forms.schema("contact");
// schema.fields → typed field defs (key, type, required, options, …)
// 2. Render your own UI from schema.fields and collect `data`,
// keyed by each field's stable `key`.
// 3. Validate locally, then submit. submit() resolves a result — it never throws.
const check = validateFormSubmission(schema, data);
if (!check.ok) return showFieldErrors(check.issues);
const result = await biab.forms.submit("contact", data, { source: "marketing-site" });
if (result.ok) {
// the submission landed in the dashboard
} else {
// result carries field-level errors — surface them
}
Use this when you need full control of the markup, or you're wiring an existing form on your site up to BIAB — the SDK makes it impossible for your form to silently drift from the schema it submits against.
2. Drop in <BiabForm> — no schema work at all
The second mechanism is the <BiabForm> component. It renders the form
exactly as it's built in the dashboard — fields, layout, validation, and
submission included — with none of the schema-matching or rewriting. You
literally drop in a component, name which form to insert, and the SDK handles the
rest.
import { createBiabDevClient } from "@biab-dev/sdk";
import { BiabForm, BiabFormProvider } from "@biab-dev/sdk/react";
const biab = createBiabDevClient({ apiKey, baseUrl });
// Pass the client directly…
<BiabForm slug="contact" client={biab} />
// …or wrap once, so any <BiabForm> below just needs a slug:
<BiabFormProvider client={biab}>
<BiabForm slug="contact" />
</BiabFormProvider>
Edit the form in the dashboard and every <BiabForm slug="contact" /> updates
with it — there's only one definition to maintain. Native bindings ship for
React, Vue, Svelte, Solid, Qwik, and Angular, plus a mountBiabForm() helper
for vanilla JS.
Use this when you want a working, validated form live with the least possible code.
Already running a form on another service?
BIAB also works as a submission endpoint for forms built elsewhere. If you already run a form on another platform or a custom backend — and you control where it submits — you can point it straight at your BIAB form and the data lands in your dashboard. It doesn't matter what tool drew the form; only that its payload reaches the endpoint.
You still create the receiving form in Site Content → Forms first, so the
submission has a home (and picks up validation, CRM mirroring, and
notifications). From there a third-party form just sends the field keys your BIAB
form defines. forms.submit(slug, data, { skipValidate: true }) relaxes the
SDK's client-side check for payloads you've already validated upstream (the
server still validates against the form's schema). That makes BIAB a highly
configurable central inbox for submissions — wherever the forms themselves live.
Test your submissions (dry-run / Postman)
A submission is just an HTTP request the SDK makes for you, so you can exercise it before — or instead of — wiring up any UI:
- Dry run through the SDK.
biab.forms.submit(slug, data, { dryRun: true })validates a payload against the live schema and returns the same result shape without writing a submission — ideal for a quick script or test. - Postman / curl / any API client. Fetch the schema first
(
forms.schema(slug)) to see the exact fieldkeys and types you must send, POST a sample submission to confirm it's accepted (or read back the validation errors), and watch it appear in your dashboard's submissions list. See the SDK reference for the exact request shape and the auth headers the SDK sets for you.
Submission results
forms.submit always resolves a FormSubmitResult — it never throws for an
expected failure. Discriminate on ok; on failure, branch on reason (a stable
string) rather than parsing messages. issues carries field-level detail for
validation failures.
const result = await biab.forms.submit("contact", data);
if (result.ok) {
result.submissionId; // stored id (absent on a dryRun)
} else {
result.status; // HTTP status (0 if the request never reached BIAB)
result.reason; // stable failure reason (see table)
result.message; // human-readable, safe to surface to the user
result.issues; // field-level errors when reason is a validation failure
}
| Status | reason | Meaning |
|---|---|---|
| 200 | — (ok: true) | Accepted and stored; submissionId returned. A dryRun returns ok: true + dryRun: true with no submissionId (validated, nothing written). |
| 400 / 422 | validation_failed | Server rejected the payload against the live schema — unknown field, wrong type/format, missing required, oversized. See issues. |
| 400 | invalid_request | The request body was malformed. |
| 401 | unauthorized | Missing / invalid / expired package API key. |
| 403 | forbidden | The key lacks scope, or the request origin isn't on the key's allowed-host list. |
| 402 | plan_required | The org's plan doesn't include forms (entitlement gate). |
| 402 | payment_required | The org's BIAB billing is past due. |
| 402 | service_suspended | The org's BIAB billing has lapsed 60+ days. |
| 429 | rate_limited | Too many submissions too fast. |
| 404 | not_found | The form doesn't exist or isn't Live (its master switch is off). Chatbot access is not required for SDK fetch/submit — only Live. |
| 5xx | server_error | BIAB failed to process the submission. |
| 0 | client_validation_failed | Local validateFormSubmission caught it before any network call. See issues. |
| 0 | network_error | The request never reached BIAB (offline, DNS, CORS, …). |
Which one should I use?
| Build your own UI | <BiabForm> | |
|---|---|---|
| You write the markup | Yes | No |
| Keeping the schema in sync | You match it (SDK-checked) | Automatic |
| Styling control | Full | Inherits the dashboard form |
| Best for | Custom designs / existing forms | Fastest drop-in |
Zero-setup contact form
When you deploy the BIAB platform, a seed script
(scripts/seed-platform-general-inquiry-form.json) creates a platform-level
"General Inquiry" form with slug general-inquiry. This form is available to
every organization on the platform — no per-org setup required.
How the fallback works
- An SDK call requests
slug = "general-inquiry" - The system checks if the calling org has its own
general-inquiryform - If yes → the org's form is returned (even if inactive — the org controls visibility)
- If no → the platform default is returned as a fallback
Only slugs in the allowlist (PLATFORM_DEFAULT_FORM_SLUGS in
forms-runtime.ts) qualify for this fallback — currently only
general-inquiry.
What this means for you
- The SDK starters ship with
slug="general-inquiry"hardcoded — they work immediately with zero dashboard setup - Submissions from the platform default are scoped to the calling org, so each org sees their own submissions in the dashboard
- If an org creates its own
general-inquiryform (with custom fields, e.g., "Company"), their copy overrides the platform default — the SDK picks it up automatically - The form is seeded with fields: Name, Subject, Email, Phone Number, Preferred
method of contact, Best time to call (conditional schedule), and Message —
plus CRM mirroring (
addsInquiry: true) and chatbot access enabled