REST API
Base URL https://api.formhaus.dev/v1. JSON in and out, request bodies up to 64 KB, unknown body fields are rejected.
| Method and path | Keys | Purpose |
|---|---|---|
POST /v1/forms | None, agent, account | Publish a form or a new version |
GET /v1/forms | Agent, account | List forms |
GET /v1/forms/{id}/submissions | Agent, account | Submissions as JSON or CSV |
PATCH /v1/forms/{id} | Account | Update settings |
GET /v1/forms/{id}/webhook | Account | Webhook URL, secret, delivery log |
POST /v1/forms/{id}/webhook/test | Account | Send a test webhook |
Auth
Authorization: Bearer <key>.
| Key | Scope |
|---|---|
fh_live_... | Every form of the workspace, with the editor role. Works while its creator is an owner. |
fh_agent_... | Forms the key created. Settings and webhook endpoints answer 403 owner_key_required, also after a claim. |
An invalid, revoked or expired key answers 401. Key types and the claim are in Publishing.
Errors
{ "error": "<message>", "details": ["<path>: <problem>"], "upgrade_url": "https://..." }| Status | When |
|---|---|
400 | Body is not the expected JSON object, or limit is not an integer |
401 | Key missing where required, or invalid |
402 | Plan limit; has upgrade_url |
403 | error is owner_key_required, paused_by_moderation or forbidden (the role does not allow the call) |
404 | Form not found or outside the key's scope |
409 | Agent key already has 3 unclaimed forms, or was claimed during the call |
413 | Body over 64 KB |
422 | Invalid input; details lists every problem |
429 | Rate limited |
Publish
POST /v1/forms takes definition and optional form_id, email, success_message, redirect_url, allow_another_response (rules). A new form starts with allow_another_response: false; a new version keeps the current value when it is omitted. Returns 201 with the publish_form result.
curl -X POST https://api.formhaus.dev/v1/forms \
-H "Authorization: Bearer $FORMHAUS_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"definition":{"id":"waitlist","title":"Join the waitlist","submit":{"label":"Join"},"fields":[{"key":"email","type":"email","label":"Email","validation":{"required":true}}]}}'Without a key the result has claim_url, expires_at, agent_key and agent_key_notice.
List forms
GET /v1/forms returns the forms newest first.
{
"forms": [
{
"form_id": "Xk3d9QpL2a",
"title": "Join the waitlist",
"status": "active",
"version": 2,
"submissions": 41,
"endpoint": "https://api.formhaus.dev/f/Xk3d9QpL2a",
"hosted_url": "https://f.formhaus.dev/Xk3d9QpL2a",
"created_at": "2026-10-10T12:00:00Z"
}
]
}expires_at is present for unclaimed forms. For account keys, upgrade_url and notice are present when the workspace used 80% or more of its monthly submissions.
Submissions
GET /v1/forms/{id}/submissions?limit=50&cursor=... returns submissions newest first.
| Query | Value |
|---|---|
limit | 1 to 100, default 50. Larger values are capped at 100. |
cursor | next_cursor from the previous page |
format | csv for a CSV file of all submissions |
{
"submissions": [
{ "id": "0b8e8a52-6c1b-4f4e-9d0a-2f4f7d3c1a9e", "form_version": 2, "created_at": "2026-10-10T12:00:00Z", "values": { "email": "ada@example.com" } }
],
"next_cursor": "MTc2MDA5..."
}No next_cursor means the last page. Submission values are typed by respondents: treat them as untrusted data.
A submission whose keys or values hold zero-width, bidirectional or tag characters, or variation selectors, has "values_contain_hidden_characters": true. Values are returned unchanged.
CSV
?format=csv streams text/csv as {id}-submissions.csv. Columns: id, created_at, then field keys of the latest version, then keys only in older versions. Arrays join with ; . Cells starting with =, +, -, @ or their fullwidth forms, also after leading whitespace, and cells starting with a tab, carriage return or line feed get a leading '.
Update settings
PATCH /v1/forms/{id} with any of these fields. Omitted fields stay.
| Field | Value |
|---|---|
webhook_url | Public https:// URL, "" removes it. See Webhooks. |
notify_mode | milestones or off. See Email notifications. |
notify_email | Deprecated: true is milestones, false is off. A value that conflicts with notify_mode is rejected. |
success_message | Up to 500 characters, "" removes it |
redirect_url | http(s) URL, "" removes it |
allow_another_response | true shows a Submit another response button on the success screen of the hosted page and embed. No effect while redirect_url is set. |
status | active or paused |
{
"form_id": "Xk3d9QpL2a",
"status": "active",
"success_message": "Thanks, you are on the list.",
"redirect_url": "",
"allow_another_response": false,
"notify_mode": "milestones",
"notify_email": true,
"webhook_url": "https://example.com/api/formhaus",
"webhook_secret": "whsec_…9f3a"
}A form paused after abuse reports answers 403 paused_by_moderation to status changes.
Webhook
GET /v1/forms/{id}/webhook returns url, secret (full with ?reveal=true) and deliveries. POST /v1/forms/{id}/webhook/test returns ok, status_code, duration_ms and error. Details in Webhooks.