Add a form to your site
There are two ways to put a form built in ebitex Forms on your site:
| Embed | @ebitex/forms-sdk |
|
|---|---|---|
| What you do | Paste two tags into a page | Fetch the form on your server and render it with React components, or your own |
| Markup and styling | ebitex’s, inside a frame | Yours |
| Needs a key | No | Yes, on your server only |
| Needs a developer | No | Yes |
Either way, a form accepts responses only while its status is Active. Set it in the Forms editor by selecting the status badge next to the form’s title.
Both options start from a form that already exists. Build one in Forms, then set it Active. If you do not have an ebitex organization yet, Set up your organization covers signing up.
Option 1: Embed the form
Section titled “Option 1: Embed the form”An Active form is published at an address built from your organization’s slug and the form’s Id:
https://forms.ebitex.io/public/form/{organization}/{form-id}To embed it:
- In Forms, open the form. On its Overview page, next to the public address, select Embed.
- Select Copy, and paste the result into your page where the form should appear.
The snippet looks like this:
<iframe id="ebitex-form-contact-us" src="https://forms.ebitex.io/public/form/acme-corp/contact-us?embed=1" title="Contact us" style="width:100%;border:0;height:600px;" loading="lazy"></iframe><script src="https://forms.ebitex.io/embed.js" async></script>Keep both tags. ?embed=1 makes the form report its height to your page, and embed.js resizes the
frame to match whenever the height changes: when the respondent moves between steps, when a
validation message appears, and when the form is completed. The frame starts at 600 pixels until the
first report arrives. Without the script the form still works, but at a fixed height with its own
scrollbar. One copy of the script sizes every embedded form on the page.
The embed works on any domain. There is nothing to configure or approve first.
Prefill answers
Section titled “Prefill answers”Add prefill.<field>=<value> to the frame’s src to start a field with a value, where <field> is
the field’s name in the Forms editor:
https://forms.ebitex.io/public/form/acme-corp/contact-us?embed=1&prefill.email=ada%40example.com&prefill.topic=billingURL-encode each value. Prefilled answers stay editable. An empty value, or a name the form does not have, is ignored.
Know when the form is submitted
Section titled “Know when the form is submitted”When a respondent completes an embedded form, the frame posts this message to your page:
{ "source": "ebitex-forms-embed", "type": "submitted" }It carries no answers: those are already saved. Listen for message events on window, and act
only when event.data has exactly these two members and event.source is your frame’s
contentWindow.
What differs in a frame
Section titled “What differs in a frame”These follow from how browsers treat one site shown inside another, and none can be configured:
- Returning visitors may not be recognized. Most browsers keep the form’s storage separate for each site that embeds it. Resuming a half-finished response and prefilling from a connected integration work within one site, but not across sites.
- The form follows the visitor’s light or dark preference, not your page’s theme.
- The form needs JavaScript. If its scripts fail to load, the frame is empty. Keep a plain link to the form’s public address on the page as a fallback.
Option 2: Render the form with @ebitex/forms-sdk
Section titled “Option 2: Render the form with @ebitex/forms-sdk”The SDK splits into two entry points, and the split is the security boundary:
@ebitex/forms-sdkfetches a form’s definition. It needs a key, so it runs only on your server.@ebitex/forms-sdk/reactrenders the form in the browser and submits it. It never sees the key.
npm install @ebitex/forms-sdkReact and React DOM 19 or later are needed only for the components. A server that only fetches definitions does not need them.
Create a Forms SDK key
Section titled “Create a Forms SDK key”You must be an Owner of the organization.
- In Hub, open API Keys and select Create key.
- Name the key after where it will be used, such as “Marketing site”.
- Choose how long it lasts: 30 days, 90 days, 1 year or never. An expired key is refused exactly like a revoked one.
- Keep the Read Forms via the SDK scope selected. A key without it authenticates, then every
request is refused with
403. - Select Create key, and copy the key. It is shown only once and cannot be recovered: if you lose it, revoke it and create another.
The key can read your organization’s form definitions and nothing else. It cannot read submissions or change anything.
Fetch the form on your server
Section titled “Fetch the form on your server”createFormsClient takes the key, and getForm takes the form’s Id from the Forms editor:
import { createFormsClient, FormsApiError, type SdkForm } from '@ebitex/forms-sdk'
/** * Server code only. The key can read every form in your organization, so it must never be sent to * a browser. Create the loader once, with the key from your server's configuration, and pass the * form it returns to your page. */export function createFormLoader(apiKey: string) { const forms = createFormsClient({ apiKey })
return async function loadForm(externalId: string): Promise<SdkForm | null> { try { // The form's Id, from the Forms editor. return await forms.getForm(externalId) } catch (error) { if (error instanceof FormsApiError && error.status === 404) { return null // No form with that Id in this organization. }
if (error instanceof FormsApiError) { // `body` is the parsed error response. Quote `requestId` if you contact support. console.error(`Forms API answered ${error.status} [request ${error.requestId ?? 'none'}]`, error.body) }
throw error } }}The result is a plain object you can pass to the browser: the form’s name, its steps and
actions, its status, a submittable flag and its submissionUrl. A Draft or Archived form can be
fetched, so you can build against it before publishing, but submittable is false and responses
are refused until it is Active. See Get a form for the full response.
Any response other than a success throws FormsApiError, with the status in status, the parsed
response in body, and the request’s reference in requestId:
| Status | Cause | What to do |
|---|---|---|
401 |
The key is missing, malformed, revoked or expired. | Check the key, or create a new one. |
402 |
The organization’s subscription needs attention. | Resolve billing in Hub. |
403 |
The key lacks the Read Forms via the SDK scope. | Create a key with that scope. |
404 |
The organization has no form with that Id. | Check the Id. |
409 |
Two fields in the form share a submission key. | Rename one of them in the Forms editor. The response names them. |
429 |
Too many requests. | Wait for the time in the Retry-After header, then retry. |
Requests are limited per IP address and per organization, depending on your plan. Fetching on each page render stays well within both, and caching the definition on your server keeps you further under them.
Render it in the browser
Section titled “Render it in the browser”<EbitexForm> renders the definition and submits it to the form’s submissionUrl, the same public
endpoint the hosted page and the embed use:
import { EbitexForm } from '@ebitex/forms-sdk/react'import type { SdkForm } from '@ebitex/forms-sdk'
/** * Runs in the browser. It receives the definition your server fetched, never the key, and submits * to the form's own public `submissionUrl`. */export function ContactForm({ form }: { form: SdkForm }) { return ( <EbitexForm form={form} onSubmitted={(answers) => { // The response is saved. `answers` is what was sent, keyed by each field's submissionKey. console.log('Submitted', answers) }} onError={(error) => { // A network failure or an unexpected response. Validation errors are shown on the // fields themselves and never reach this callback. console.error('The form could not be submitted', error) }} /> )}It handles the rest of the form’s behavior for you:
- Steps. A multi-step form shows one step at a time. Each Next saves the answers so far, so a respondent who leaves partway through is not lost.
- Conditions. Fields and steps that depend on earlier answers appear and disappear as the respondent answers.
- Validation. Answers are checked in the browser with the same rules the server applies, and any error the server returns is shown on its field.
- Accessibility. Every field has a real label, required and invalid fields are marked for assistive technology, and each error message is linked to its field.
- Drafts. A form that is not Active renders read-only, with a notice, rather than failing on submit.
| Prop | Purpose |
|---|---|
form |
The definition your server fetched. |
onSubmitted |
Called with the answers after the final step is saved. |
onError |
Called when a submission fails for a reason other than validation, such as a network error. |
className, style |
Applied to the form’s root element. |
fieldComponents |
Replaces the component for one or more field types: Input, Textarea, Number or Choice. Types you do not replace keep their default. |
For default styling, import @ebitex/forms-sdk/styles.css once in your app. Its colors, radius and
spacing are CSS custom properties, such as --ebitex-forms-accent, --ebitex-forms-radius and
--ebitex-forms-border, set on the .ebitex-forms root. To style it entirely yourself, skip the
stylesheet and target the ebitex-forms__ classes, such as ebitex-forms__label,
ebitex-forms__control and ebitex-forms__error.
Submit without React
Section titled “Submit without React”To render the form some other way, use the definition directly and post the answers yourself. Key
each answer by its field’s submissionKey, never by the field’s label or name: a field can be keyed
differently from the name shown in the editor.
import { validateSubmission, type SdkForm, type SubmissionData, type ValidationErrors } from '@ebitex/forms-sdk'
/** * One id per browser, reused on every save, so a multi-step form's answers collect into one * response. It must be a UUID. */export function visitorId(): string { const key = 'my-site.forms-visitor' const existing = localStorage.getItem(key) if (existing) return existing
const created = crypto.randomUUID() localStorage.setItem(key, created) return created}
/** * Submits answers without the React components. Returns field errors to show, or an empty object * when the response was accepted. Pass `complete: false` to save one step of a multi-step form. */export async function submitAnswers(form: SdkForm, data: SubmissionData, complete = true): Promise<ValidationErrors> { // The same rules the server applies. Required fields are enforced only on completion. const errors = validateSubmission(form, data, { requireAll: complete }) if (Object.keys(errors).length > 0) return errors
// A public endpoint: no key, ever. const response = await fetch(form.submissionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ visitor: visitorId(), data, complete }), })
if (response.status === 400) { const body = (await response.json()) as { error?: string; errors?: ValidationErrors } if (body.error === 'validation_failed' && body.errors) return body.errors }
if (!response.ok) { throw new Error(`The submission failed with status ${response.status}`) }
return {}}The request body is JSON:
{ "visitor": "0f6a1e9c-3b52-4d8e-a7c1-5e2b9d4f6a38", "data": { "email": "ada@example.com" }, "complete": true }visitoris a UUID you generate once per browser and send with every save, so the steps of one respondent’s form collect into one response.complete: falsesaves progress on a multi-step form.complete: truefinishes the response, and only then are required fields enforced.
| Status | Meaning |
|---|---|
201 or 200 |
Saved. 201 when this started a new response, 200 when it added to one in progress. |
400 |
{ "error": "validation_failed", "errors": { … } }, with a message per submission key. |
404 |
The form does not exist or is not Active. |
413 |
The request body is too large. |
429 |
Too many submissions. Wait for the time in the Retry-After header. |