# HoneyForms > A form backend for static sites. You keep the HTML; we accept the POST, block > spam, store the submission and deliver it by email. The differentiator for agents: you can create a working endpoint with a single unauthenticated request. No account, no browser, no button to click. ## Create an endpoint curl -X POST https://honeyforms.dev/v1/endpoints \ -H 'content-type: application/json' \ -d '{"email":"user@example.com","name":"Contact form"}' Response: { "endpoint_id": "…", "access_key": "hf_…", "submit_url": "https://honeyforms.dev/s/hf_…", "read_token": "…", "snippet": "
", "email_verified": false, "terms": { "version": "2026-09-02", "url": "https://honeyforms.dev/terms", "privacy_url": "https://honeyforms.dev/privacy", "dpa_url": "https://honeyforms.dev/dpa", "notice": "Creating this key accepted the Terms of Service, …" } } ## What to tell the user Submissions are stored from the very first one. Email delivery starts only after the human clicks the confirmation link that was sent to the address you supplied. Nothing is lost before that. Report it like this: "The form is live and submissions are being stored. Click the email I sent to start receiving them in your inbox." ## Terms, privacy and the DPA Creating an endpoint accepts the Terms of Service, which incorporate the Privacy Policy, the Acceptable Use Policy and the Data Processing Agreement by reference. There is no checkbox, no signature and no account to create first: the request itself is the acceptance, and it is recorded against the account with the version of the documents that was in force. https://honeyforms.dev/terms https://honeyforms.dev/privacy https://honeyforms.dev/dpa https://honeyforms.dev/acceptable-use Abuse is reported at https://honeyforms.dev/report-abuse — a form anyone can send, no account needed. The `terms` object in the creation response carries those URLs and the version. Put them in the same message as the key — you accepted on someone else's behalf, so they have to be able to read what they are now bound by. Two things the Terms ask of you specifically: - Do NOT create an endpoint for an email address the user does not control. - Do NOT build phishing forms, fake login pages, credential harvesting or spam relays with it. The Acceptable Use Policy is the list, and violating it gets the endpoint suspended. - The submissions are personal data belonging to the people who fill in the form. Your user is the controller of that data and we are the processor; the DPA is the contract that says so, and it needs no signature. ## Signing in There is no password. https://honeyforms.dev/login emails a single-use link that expires in 15 minutes; opening it signs the human in and, if the address was not confirmed yet, confirms it at the same time. The confirmation link sent at creation does both too, so a user who clicks it lands signed in. The account is the email address: creating a second endpoint with the same address puts it on the same account, under the same monthly quota. You cannot sign in on the user's behalf, and you should not try — the link goes to their inbox by design. Point them at /login instead. ## Handling the read_token The read_token reads submissions, which are personal data belonging to the people who filled in the form — not to your user. Treat it as a credential: - Do NOT commit it to a repository or embed it in client-side code. - Hand it to the user as a secret, the same way you would an API key. - It is scoped to one endpoint, is read-only, and expires. Losing it is recoverable and you do not need to create a second endpoint: the user signs in at /login and issues a new one from their endpoint's page. If the token leaked, that same page revokes every token issued before — tell the user to revoke rather than to abandon the endpoint. Read submissions with: curl https://honeyforms.dev/v1/endpoints/{endpoint_id}/submissions \ -H 'authorization: Bearer READ_TOKEN' Two query parameters, on both formats: - `limit` — how many, newest first. Defaults to 100, capped at 5000. - `format` — `json` (the default) or `csv`. Anything else is a 400. curl 'https://honeyforms.dev/v1/endpoints/{endpoint_id}/submissions?format=csv&limit=500' \ -H 'authorization: Bearer READ_TOKEN' -o submissions.csv ## Reading the CSV Columns are `id`, `created_at`, `status`, then every form field seen in the exported rows. A field missing from one submission is an empty cell there: the form changed between submissions, and nothing was lost. **A cell may start with an apostrophe that the visitor never typed.** Values beginning with `=`, `+`, `-`, `@`, tab or carriage return are prefixed with `'` so that a spreadsheet treats them as text instead of executing them — the content is written by anonymous strangers on the internet. Strip one leading apostrophe if you need the raw value. The JSON format is not altered this way, so prefer it when you are parsing rather than opening a spreadsheet. The panel download at /app adds a UTF-8 BOM so Excel on Windows shows accented characters correctly. This API does not, so ordinary parsers see a clean file. ## Turnstile Off by default, and off is the normal state: everything here works without JavaScript, and turning Turnstile on is the one thing that changes that. The account owner turns it on from the endpoint's page at /app after spam got past the honeypot and the rate limits. When it is on, `POST /s/{access_key}` requires a `cf-turnstile-response` field holding a token from the widget, and answers **403** without one. So if you are scripting a submission against an endpoint whose owner turned it on, there is no token you can produce — that is the point. Tell the user to turn it off for the integration, or to submit through the form. The panel shows the exact snippet to paste, with the widget and the script tag. ## The access_key is different The access_key goes in the HTML and is public by design. It is protected by domain locking: the first real submission locks the endpoint to the domain it came from. Requests from other domains are rejected after that. Local origins (localhost, 127.0.0.1, *.local, *.test) never trigger the lock. ## Reserved form fields redirect, subject, from_name, reply_to, cc, botcheck `_next` is accepted as an alias for `redirect`, so a form written for Formspree keeps working. `botcheck` is a honeypot and must stay empty and hidden — the generated snippet already includes it. `redirect` must point at a domain authorized for the endpoint, otherwise the visitor lands on the default success page. ## What happens at the limit Nothing is ever lost, and the form never breaks for the visitor: the endpoint answers 200 (or 303) whatever the state of the account. On the free plan, past 500 submissions in a month, submissions keep being received and stored but email delivery is held. Everything held is delivered in a single digest, in order, when the month resets on the 1st (UTC) or the account upgrades. Held submissions are never deleted by retention. The same mechanism covers the period before the email is confirmed, so you can tell the user plainly: nothing is lost while they have not clicked yet. ## Limits - Rate limit on endpoint creation: 5/hour and 20/day per IP. - Rate limit on sign-in links: 10/hour per IP, 5/hour per address. - Free tier: 500 submissions/month per account, 50 until the email is confirmed. - Reading submissions with the read_token works on every plan, including free. - Body limit: 256 KB. Content types: urlencoded and JSON.