---
title: "Form builder API or submission endpoint?"
description: "Decide whether a plain form endpoint is enough or whether your product needs shared schema, native renderers, uploads, response review, and versioned changes."
order: 32
type: "guide"
topic: "Evaluating tools"
tags:
  - "form builder API"
  - "form submission API"
  - "form backend"
  - "schema"
  - "responses"
keywords:
  - "form builder api"
  - "form submission api"
  - "form backend api"
updated: "2026-08-07"
---

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

| Situation | Plain submission endpoint | Form builder API and SDK |
| --- | --- | --- |
| One stable contact form | Usually enough | Optional |
| Native form inside an authenticated product | You build the full UI and context path | SDK renders or powers native controls |
| Non-developers edit questions after launch | You build an editor and migration rules | Draft and published versions are separate |
| Client and server validation must agree | You maintain both copies | Shared schema drives both paths |
| File uploads | You build sessions, provider access, retries, verification, and cleanup | Storage adapter and upload lifecycle are included |
| Response review and CSV export | You build the admin surface and export semantics | Response workspace and exports are included |
| Webhooks and integrations | You build signing, retries, status, and idempotency guidance | Accepted responses can enter the delivery workflow |
| Historical answers must survive schema edits | You version and interpret every change | Responses 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.

## Related

- [Code-first example](/examples#code-first): Inspect a schema rendered with application-owned components.
- [Form builder API overview](/form-builder-api)
- [Management API reference](/docs/api)
- [Code-defined forms](/docs/authoring)
- [The request lifecycle of a native product form](/guides/native-form-request-lifecycle)
