Skip to content

Add forms to 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"
}
Field Type Description
data.hashId str Unique submission identifier
data.date str Submission timestamp (YYYY-MM-DD HH:mm:ss)
data.blocks dict All submitted field values
redirectUrl str Thank 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 Code HTTP Status Description
FORM_NOT_FOUND 404 Form ID doesn’t exist or was deleted
FORM_DISABLED 403 Form is disabled by owner
MISSING_API_KEY 401 API key required but not provided
EMPTY_SUBMISSION 400 No fields with values submitted
FI_SCHEMA_FORMAT_EMAIL 400 Invalid email format
FI_RULES_PHONE_INVALID 400 Invalid phone number format
FI_SCHEMA_RANGE_RATING 400 Rating not between 1-5
FI_DATA_COUNTRY_INVALID 400 Invalid country code
TOO_MANY_REQUESTS 429 Rate 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

View this page as Markdown