Skip to content

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 pathKeysPurpose
POST /v1/formsNone, agent, accountPublish a form or a new version
GET /v1/formsAgent, accountList forms
GET /v1/forms/{id}/submissionsAgent, accountSubmissions as JSON or CSV
PATCH /v1/forms/{id}AccountUpdate settings
GET /v1/forms/{id}/webhookAccountWebhook URL, secret, delivery log
POST /v1/forms/{id}/webhook/testAccountSend a test webhook

Auth ​

Authorization: Bearer <key>.

KeyScope
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 ​

json
{ "error": "<message>", "details": ["<path>: <problem>"], "upgrade_url": "https://..." }
StatusWhen
400Body is not the expected JSON object, or limit is not an integer
401Key missing where required, or invalid
402Plan limit; has upgrade_url
403error is owner_key_required, paused_by_moderation or forbidden (the role does not allow the call)
404Form not found or outside the key's scope
409Agent key already has 3 unclaimed forms, or was claimed during the call
413Body over 64 KB
422Invalid input; details lists every problem
429Rate 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.

bash
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.

json
{
  "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.

QueryValue
limit1 to 100, default 50. Larger values are capped at 100.
cursornext_cursor from the previous page
formatcsv for a CSV file of all submissions
json
{
  "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.

FieldValue
webhook_urlPublic https:// URL, "" removes it. See Webhooks.
notify_modemilestones or off. See Email notifications.
notify_emailDeprecated: true is milestones, false is off. A value that conflicts with notify_mode is rejected.
success_messageUp to 500 characters, "" removes it
redirect_urlhttp(s) URL, "" removes it
allow_another_responsetrue shows a Submit another response button on the success screen of the hosted page and embed. No effect while redirect_url is set.
statusactive or paused
json
{
  "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.

Formhaus is MIT licensed. Legal