Documentation
Everything you need to send submissions to Intake and get them out again.
Sending submissions
Each form has its own address, shown on its Install tab:
POST https://intake.starstorm.dev/api/form/<form id>/submission
It accepts three kinds of body:
- An HTML form post (
application/x-www-form-urlencoded): the visitor is redirected to your thank-you page, or Intake's own. - JSON (
application/json), or any post withAccept: application/json: the answer is JSON. - File uploads (
multipart/form-data), on plans with uploads: each file field becomes an object with its name, type, size and a download link.
<form action="https://intake.starstorm.dev/api/form/<form id>/submission" method="POST"> <input name="email" type="email" required> <textarea name="message"></textarea> <!-- the honeypot: hide it with CSS; bots fill it in --> <input name="fc_honeypot" style="display:none" tabindex="-1" autocomplete="off"> <button>Send</button> </form>
const res = await fetch("https://intake.starstorm.dev/api/form/<form id>/submission", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "ada@example.com", message: "Hello" }),
});
const submission = await res.json(); // { id, formId, data, domain, createdAt, ... }
Errors
JSON callers get an HTTP status and {"message": "...", "name": "..."}:
| Status | name | Why |
|---|---|---|
| 400 | badRequestError | The sending site is not in the form's allowed domains, or the body is not valid |
| 403 | forbiddenError | The form is not accepting submissions |
| 404 | notFoundError | No such form |
| 422 | validationError | The form enforces its expected fields; errors says which failed |
| 429 | rateLimitError | Too many posts from one address or to one form; retry shortly |
Over your monthly limit, submissions are held, not refused: the answer is 200 with "held": true, and the submission appears once the quota allows.
Spam protection
Hosted pages include it automatically. On your own pages, add the honeypot above, and turn on proof of work under the form's spam rules, then mark the form and load the script:
<form action="https://intake.starstorm.dev/api/form/<form id>/submission" method="POST"
data-intake-pow="https://intake.starstorm.dev/api/form/<form id>/challenge">
...
</form>
<script src="https://intake.starstorm.dev/pow.js" defer></script>
<!-- with fetch(): body.altcha = await IntakePow.solve(challengeUrl) -->
Anything caught is stored as spam with the reason, never dropped, and can be released from the inbox.
One response per person
For surveys and votes where answers are anonymous but each person may answer only once. Under the form's Settings → One response per person, download a set of links (a CSV) and send one to each person. Each link works once. Intake keeps only a fingerprint of each link and whether it has been used, so it cannot tell which link sent which answer, and neither can you.
The hosted page handles the link for you. On your own page, the link's code arrives as ?t= in the address: pass it on by adding it to the endpoint (the settings page has a short script for HTML forms). A post with a missing, unknown or used link gets 403 with "name": "linkUsedError".
Opening, closing and results
Under the form's Settings, set when it opens and closes, and how many responses it takes. Outside those times, or once it is full, posts get 403 with "name": "formClosedError" and a message you can show as it is; the hosted page shows it instead of the form. Spam does not take a place.
The Results tab counts the answers to choice fields (select and checkbox under Expected fields), with percentages. For encrypted forms the counting happens in your browser after you unlock the vault.
Anonymous follow-up
For whistleblowing and other reports where people stay anonymous but need an answer. Turn on Settings → Anonymous follow-up and each person who submits is given a follow-up code, once. At https://intake.starstorm.dev/f/<form id>/status they enter it to read your replies and answer, with no account or email address. You reply from the submission on the Submissions tab; they see "The team", not your name.
Browser posts show the code on Intake's thank-you page (with a link on to your own). With fetch(), the response has "followUpCode": show it to the person, and do not store or log it. Intake keeps only a fingerprint, so a lost code cannot be recovered. Not yet available on end-to-end encrypted forms.
Your team is emailed when a reporter replies, and reminded daily about reports with no reply after 5 days or no update for 80 days, in line with the EU Whistleblowing Directive's 7-day acknowledgement and 3-month feedback. The emails never include what anyone wrote.
API
Create a key under API keys and send it in the Authorization header. Keys can be limited to one form and to these scopes:
forms:read: forms, actions, domains and your plansubmissions:read: submissions (andintake-cli listen)forms:write: create and change forms, actions and domains; send test submissions
curl -H "Authorization: <key>" "https://intake.starstorm.dev/public/v1/form?limit=10" curl -H "Authorization: <key>" "https://intake.starstorm.dev/public/v1/form/<form id>/submission?getSpam=true"
The full reference is an OpenAPI 3.1 description at /public/v1/openapi.json; import it into Postman, Insomnia or your code generator. Errors use the same {"message", "name"} shape as above.
AI agents (MCP)
Intake is also an MCP server, so an agent such as Claude can set up forms and actions for you:
claude mcp add --transport http intake https://intake.starstorm.dev/mcp --header "Authorization: Bearer <key>"
Give the agent a key with forms:write. Add submissions:read only if it should read what visitors typed: that text is untrusted and could try to instruct the agent. Encrypted forms cannot be read by agents at all.
Next
- Webhooks and signatures: what Intake sends, and how to check it came from us
- Decrypting encrypted forms in your own systems
- The Intake CLI: real submissions on your machine while you build