Skip to content

Handle form submissions with FastAPI

Learn how to handle form submissions in FastAPI using the Forminit Python SDK with async support.


  1. Create a Forminit account at forminit.com
  2. Create a form in your dashboard
  3. Set authentication mode to Protected in Form Settings
  4. Create an API token from Account → API Tokens

pip install forminit fastapi uvicorn python-dotenv

Set authentication to Protected for server-side integrations. This enables higher rate limits and requires the x-api-key header.

Forminit Authentication Mode Protected

3. Create an API Token and Add to Environment

Section titled “3. Create an API Token and Add to Environment”

Generate your secret API token from Account → API Tokens in the Forminit dashboard.

# .env
FORMINIT_API_KEY="fi_your_secret_api_key"
my-fastapi-app/
├── main.py
└── .env

Forminit uses a block-based system to structure form data. Each submission contains an array of blocks representing different field types.

For complete documentation on all available blocks, field naming conventions, and validation rules, see the Form Blocks Reference.

FastAPI works best with the AsyncForminitClient for non-blocking I/O. Use a lifespan context manager to manage the client lifecycle:

# main.py
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Form, Request, HTTPException
from fastapi.responses import HTMLResponse
from forminit import AsyncForminitClient
from dotenv import load_dotenv

load_dotenv()

FORM_ID = "YOUR_FORM_ID"
forminit_client: AsyncForminitClient = None


@asynccontextmanager
async def lifespan(app: FastAPI):
    global forminit_client
    forminit_client = AsyncForminitClient(api_key=os.environ["FORMINIT_API_KEY"])
    yield
    await forminit_client.close()


app = FastAPI(lifespan=lifespan)


@app.get("/", response_class=HTMLResponse)
async def index():
    return """
    <form action="/submit" method="POST">
      <input type="text" name="fullName" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required></textarea>
      <button type="submit">Send</button>
    </form>
    """


@app.post("/submit")
async def submit(
    request: Request,
    fullName: str = Form(...),
    email: str = Form(...),
    message: str = Form(...),
):
    # Pass user info for geolocation and attribution tracking
    forminit_client.set_user_info(
        ip=request.headers.get("x-forwarded-for", request.client.host),
        user_agent=request.headers.get("user-agent"),
        referer=request.headers.get("referer"),
    )

    response = await forminit_client.submit(FORM_ID, {
        "blocks": [
            {
                "type": "sender",
                "properties": {
                    "email": email,
                    "fullName": fullName,
                },
            },
            {
                "type": "text",
                "name": "message",
                "value": message,
            },
        ],
    })

    if response.get("error"):
        raise HTTPException(status_code=400, detail=response["error"]["message"])

    return {"success": True, "submissionId": response["data"]["hashId"]}
uvicorn main:app --reload

FastAPI automatically generates interactive API docs at http://localhost:8000/docs.


Use Pydantic models for type-safe JSON request bodies:

from pydantic import BaseModel


class ContactSubmission(BaseModel):
    email: str
    firstName: str | None = None
    lastName: str | None = None
    message: str
    plan: str | None = None


@app.post("/api/direct-submit")
async def direct_submit(request: Request, payload: ContactSubmission):
    forminit_client.set_user_info(
        ip=request.headers.get("x-forwarded-for", request.client.host),
        user_agent=request.headers.get("user-agent"),
        referer=request.headers.get("referer"),
    )

    response = await forminit_client.submit(FORM_ID, {
        "blocks": [
            {
                "type": "sender",
                "properties": {
                    "email": payload.email,
                    "firstName": payload.firstName,
                    "lastName": payload.lastName,
                },
            },
            {
                "type": "text",
                "name": "message",
                "value": payload.message,
            },
            {
                "type": "select",
                "name": "plan",
                "value": payload.plan,
            },
        ],
    })

    if response.get("error"):
        raise HTTPException(status_code=400, detail=response["error"]["message"])

    return {"success": True, "submissionId": response["data"]["hashId"]}

You can also submit using flat fi- prefixed keys:

@app.post("/submit-flat")
async def submit_flat(request: Request):
    form_data = await request.form()
    flat_data = dict(form_data)

    forminit_client.set_user_info(
        ip=request.headers.get("x-forwarded-for", request.client.host),
        user_agent=request.headers.get("user-agent"),
        referer=request.headers.get("referer"),
    )

    response = await forminit_client.submit(FORM_ID, flat_data)

    if response.get("error"):
        raise HTTPException(status_code=400, detail=response["error"]["message"])

    return {"success": True, "submissionId": response["data"]["hashId"]}

With an HTML form using fi- prefixed field names:

<form action="/submit-flat" method="POST">
  <input type="text" name="fi-sender-fullName" placeholder="Name" required />
  <input type="email" name="fi-sender-email" placeholder="Email" required />
  <textarea name="fi-text-message" placeholder="Message" required></textarea>
  <button type="submit">Send</button>
</form>

{
    "data": {
        "hashId": "7LMIBoYY74JOCp1k",
        "date": "2026-01-01 21:10:24",
        "blocks": {
            "sender": {
                "firstName": "John",
                "lastName": "Doe",
                "email": "john@example.com"
            },
            "message": "Hello world"
        }
    },
    "redirectUrl": "https://forminit.com/thank-you"
}
FieldTypeDescription
data.hashIdstrUnique submission identifier
data.datestrSubmission timestamp (YYYY-MM-DD HH:mm:ss)
data.blocksdictAll submitted field values
redirectUrlstrThank you page URL
{
    "error": {
        "error": "FI_SCHEMA_FORMAT_EMAIL",
        "code": 400,
        "message": "Invalid email format"
    }
}

@app.post("/submit")
async def submit(
    request: Request,
    fullName: str = Form(...),
    email: str = Form(...),
    message: str = Form(...),
):
    forminit_client.set_user_info(
        ip=request.headers.get("x-forwarded-for", request.client.host),
        user_agent=request.headers.get("user-agent"),
        referer=request.headers.get("referer"),
    )

    response = await forminit_client.submit(FORM_ID, {
        "blocks": [
            {
                "type": "sender",
                "properties": {"email": email, "fullName": fullName},
            },
            {
                "type": "text",
                "name": "message",
                "value": message,
            },
        ],
    })

    if response.get("error"):
        error = response["error"]
        error_code = error.get("error")

        if error_code == "FI_SCHEMA_FORMAT_EMAIL":
            raise HTTPException(status_code=400, detail="Invalid email address")
        elif error_code == "FI_RULES_PHONE_INVALID":
            raise HTTPException(status_code=400, detail="Invalid phone number format")
        elif error_code == "TOO_MANY_REQUESTS":
            raise HTTPException(status_code=429, detail="Please wait before submitting again")
        else:
            raise HTTPException(status_code=400, detail=error.get("message"))

    return {"success": True, "submissionId": response["data"]["hashId"]}
Error CodeHTTP StatusDescription
FORM_NOT_FOUND404Form ID doesn’t exist or was deleted
FORM_DISABLED403Form is disabled by owner
MISSING_API_KEY401API key required but not provided
EMPTY_SUBMISSION400No fields with values submitted
FI_SCHEMA_FORMAT_EMAIL400Invalid email format
FI_RULES_PHONE_INVALID400Invalid phone number format
FI_SCHEMA_RANGE_RATING400Rating not between 1-5
FI_DATA_COUNTRY_INVALID400Invalid country code
TOO_MANY_REQUESTS429Rate limit exceeded

  1. Store API keys in environment variables — Never hardcode keys in source code
  2. Use .gitignore — Exclude .env files from version control
  3. Use Pydantic models — Validate request payloads automatically
  4. Use HTTPS — Always use secure connections in production
  5. Use lifespan context manager — Ensures the HTTP client is properly managed
  6. Call set_user_info() — Pass the real user’s IP, user agent, and referer for accurate tracking