Skip to main content
Menu
On this page

Form builder API or submission endpoint?

Decide whether a plain form endpoint is enough or whether your product needs shared schema, native renderers, uploads, response review, and versioned changes.

A submission endpoint solves one narrow problem: accept values from a form and put them somewhere. That is often enough for a small contact form whose UI, validation, delivery, and future changes already belong to the application.

A form builder API solves a wider lifecycle. The browser, server, editor, response workspace, exports, uploads, and downstream delivery need to interpret the same form even after its labels or options change.

Choose based on the lifecycle you need to own, not the number of endpoints in the product.

The short decision

SituationPlain submission endpointForm builder API and SDK
One stable contact formUsually enoughOptional
Native form inside an authenticated productYou build the full UI and context pathSDK renders or powers native controls
Non-developers edit questions after launchYou build an editor and migration rulesDraft and published versions are separate
Client and server validation must agreeYou maintain both copiesShared schema drives both paths
File uploadsYou build sessions, provider access, retries, verification, and cleanupStorage adapter and upload lifecycle are included
Response review and CSV exportYou build the admin surface and export semanticsResponse workspace and exports are included
Webhooks and integrationsYou build signing, retries, status, and idempotency guidanceAccepted responses can enter the delivery workflow
Historical answers must survive schema editsYou version and interpret every changeResponses retain their form-version context

If the left column is small and stable, use the simpler system. Infrastructure is useful only when it removes work you would otherwise have to maintain.

What a plain endpoint leaves with your application

A conventional custom form might send JSON to one route:

ts
const response = await fetch("/api/contact", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ email, message }),
});

if (!response.ok) {
  // Map the server error back to the correct field and retry state.
}

The code is short because the rest of the contract is elsewhere. The application still owns:

  • Field definitions in the UI and server validator.
  • Conditional visibility and what “required” means for a hidden field.
  • Stable IDs when labels or options change.
  • Spam handling and duplicate behavior.
  • Upload sessions and storage authorization.
  • The response database, review interface, exports, and deletion behavior.
  • Notification or webhook delivery and retries.

That is reasonable when the form is one small component of a system that already provides those capabilities.

What changes with a shared form contract

Fillo starts from one published schema. The React and DOM SDKs resolve that schema and render native controls, or expose headless state for a custom design system. The public submission path validates the same field IDs and conditions again on the server.

The host application still owns its route, components, account authorization, and product analytics. Fillo owns the form definition, validation contract, uploads, accepted response, versions, exports, and delivery workflow.

There are two browser/server capabilities:

  • A pk_ publishable key can resolve or, when policy permits, stage code-defined forms from approved browser origins. Published forms can be fetched and submitted by form ID without that key.
  • An fsk_ workspace key can read or manage workspace resources from a trusted server, limited by its scopes.

Management routes are not CORS-open. A workspace key placed in browser code fails instead of quietly granting management authority.

Read accepted responses from your backend

Create a scoped key with responses:read, keep it in a server-only environment variable, and request a keyset page:

ts
const response = await fetch(
  "https://fillo.so/api/v1/manage/forms/product-feedback/responses?limit=100",
  {
    headers: {
      Authorization: `Bearer ${process.env.FILLO_API_KEY}`,
    },
  },
);

if (!response.ok) {
  throw new Error(`Fillo returned ${response.status}`);
}

const { data, nextCursor } = await response.json();

Pass nextCursor back as cursor for the next page. The API returns accepted responses; held submissions remain outside normal response lists until they are released.

Use a signed webhook when a downstream system should react to every accepted response. Use the management API when your server needs to query, filter, summarize, export, or display responses on demand.

Builder-managed or code-managed?

The API decision is separate from who authors the schema:

  • Builder-managed: a teammate edits a draft in Fillo and publishes it after review.
  • Code-managed: defineForm() or Fillo JSX stages the typed schema from the repository, then a person or approved agent publishes it.

Both paths preserve a draft-versus-published boundary. Code ownership is useful when the schema belongs in review and tests; builder ownership is useful when operational teammates should change copy or questions without a code deployment.

Do not mix them casually on the same form. Decide which source owns structural changes, then use the other surfaces for the settings they are designed to manage.

A practical threshold

Stay with a plain endpoint when all of these are true:

  • The form has few fields and changes rarely.
  • Your application already owns server validation and the response database.
  • You do not need a separate response workspace.
  • Uploads, retries, exports, and downstream delivery are absent or already solved.
  • Only developers change the form, and historical interpretation is trivial.

Consider a form API and SDK when two or more of these start repeating:

  • The same fields exist in UI code, server validation, exports, and admin views.
  • Product or operations teammates need safe edits after launch.
  • Uploaded files must remain attached to a response.
  • Several product routes need the same form infrastructure but different layouts.
  • Respondent identity or account context matters.
  • Webhooks, integrations, response state, or delivery failures need to be visible.
  • Old responses must remain readable after schema changes.

The goal is not to replace a ten-line fetch. It is to stop rebuilding the lifecycle that surrounds it.

Migrate one form without rebuilding the product

Start with a form whose operational cost is already visible: onboarding, client intake, an application, or in-app feedback. Keep the host route and layout, replace only the form contract and submission workflow, then verify one accepted response end to end.

Do not migrate every contact form to prove the architecture. A successful first form should demonstrate less duplicated schema, a useful response record, or a handoff the team no longer has to maintain manually.

Updated

Was this page helpful?