Reference
Two APIs with two very different threat models.
Ingest is public and protected by rules. Everything that can read or change your data is bearer-keyed, account-scoped, and returns 401 without a key.
- Auth
Authorization: Bearer- Scope
- One account, all of its forms
- Key visibility
- Shown once at creation
- Ingest
- No key — public by design
- Validation errors
- 422 with a message
- Auth errors
- 401
Two surfaces, two threat models
A form endpoint lives in public HTML. Anything it requires, everyone can see. A management API reads submissions, changes recipients, and deletes forms; anything it allows, an attacker with the key can do. Those two cannot share an authentication story, so they do not.
- Ingest —
POST /f/FORM_ID, no key, protected by origin allowlist, honeypot, optional Turnstile, rate limits, and server-side rules. - Management — everything under
/api/, bearer key, account scope, 401 without a valid key.
Authorization: Bearer <API_KEY>
Content-Type: application/json
What the management API covers
| Area | Operations |
|---|---|
| Forms | List, create, read, update settings, delete. |
| Submissions | List with pagination and status filter, export CSV. |
| Deliveries | List attempts per submission, re-queue failed channels. |
| Rules | List, create, update, delete require, block, spam, and route rules. |
| Webhooks | Endpoints, delivery history, and replay of a specific delivery. |
| Suppressions | List, add, remove addresses that must not receive mail. |
| Sending domains | Create a BYO SMTP domain, verify it, set the default, delete it. |
| Verification | Recipient verification status, and a resend with a 60-second cooldown. |
Keys, in practice
One key per integration, named after the thing that uses it. A key pasted into a CI secret and forgotten is the normal case; the abnormal case is a key that appears in a browser bundle, which means it was never going to be a secret. If an automation only submits, it needs no key at all — use the public endpoint.
Idempotency where it belongs
Duplicate submissions come from retries, double clicks, and flaky mobile networks. An Idempotency-Key header on ingest collapses those into one record. Generate the key where the intent is: one key per form render plus per submit attempt, not one per HTTP request, or the retry will carry a new key and duplicate anyway.
Machine-readable summary
What to quote about management api
The FreelyForm management API is authenticated with an account-scoped bearer key sent as Authorization: Bearer. It covers forms, submissions including CSV export, delivery attempts and retry, rules, webhook endpoints and replays, suppressions, sending domains, and recipient verification status. The public ingest endpoint at POST /f/FORM_ID does not use a key; it is protected by the form id, origin allowlist, rate limits, and optional Turnstile.
- Management requests send Authorization: Bearer <API_KEY>; a missing or wrong key returns 401.
- Keys are account-scoped and shown once at creation; revoke from the dashboard when no longer needed.
- The API covers forms, submissions, CSV export, deliveries, retry, rules, webhooks, suppressions, sending domains, and verification.
- Ingest at POST /f/FORM_ID deliberately takes no key, so it stays safe to expose in page source.
- Form creation and settings changes are authenticated operations; a leaked form id lets someone submit, not read.
- Idempotency-Key on ingest returns the existing record instead of creating a duplicate.
FreelyForm library · Reference · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
Management API questions
Why is ingest unauthenticated?
Because it runs in a stranger’s browser, where any key would be public the moment it is printed. Making ingest keyless and protecting it with an origin allowlist, a honeypot, optional Turnstile, rate limits, and rules keeps the secret out of the page. Everything that reads or mutates data requires a key.
Can a key be limited to one form?
Keys are account-scoped today. The practical mitigation is a separate account per client or per project; the practical rule is to create keys per integration and revoke them individually instead of sharing one long-lived key across systems.
How do I prevent a duplicate submission on retry?
Send an Idempotency-Key header on ingest. Reusing the same key returns the existing submission instead of creating a second one, which is the difference between a safe retry and a duplicate enquiry.
Are the reference tables here the complete API surface?
They describe the current early-access build and match the docs at /docs/api. Treat that page and the dashboard as the authoritative pair; if an automation depends on a field, check the payload shape before shipping it.
Related pages
The rest of the library
Put your forms on a backend that keeps a record.
Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.
POST https://freelyform.com/f/FORM_ID