Submit endpoint.
One POST per submission, and it is the only endpoint your visitors ever touch. It takes urlencoded, multipart and JSON bodies, stores everything you send, mails it to the form's recipients and hands the rest to the queue — webhooks, integrations, autoresponder.
The two routes
Identical in behaviour — pick whichever fits. With the key in the path the form markup stays clean; with the key in the body a single endpoint can serve several forms.
Body formats
All three are accepted. Use multipart only when there are files — it is bigger, and without an upload there is nothing to gain.
| Content-Type | When |
|---|---|
application/json |
Programs, fetch, server-to-server. |
application/x-www-form-urlencoded |
Classic HTML form post and AJAX without files. |
multipart/form-data |
As soon as a file is attached. |
What comes back depends on the Accept header. A request that asks for text/html — a browser posting a form — is answered with a 303 to the thank-you page or to your redirect target. Anything else gets JSON. Sending Accept: application/json or the header X-Requested-With always forces JSON.
Your own fields
Every field that is not on the reserved list is stored as submitted and shown in the notification mail, in the dashboard and in the read API. There is no schema to declare beforehand — name your fields as you like.
One convention is worth keeping: a field named email is used as the reply address of the notification mail and as the recipient of the autoresponder.
Reserved field names
These names steer the pipeline instead of being stored. They never appear in the stored data, never in the mail body and never in the read API.
| Field | Effect |
|---|---|
access_key / accessKey |
The form's access key, when it is not in the URL. |
subject |
Subject of the notification mail. Single line, max. 200 characters. |
from_name |
Sender name of the notification mail. Max. 100 characters. |
replyto / reply_to |
Reply address. Must be a single valid address. |
ccemail |
Comma-separated CC addresses, at most five. |
redirect |
Target for the 303 after a successful submission. |
botcheck / _gotcha |
Honeypot. Anything filled in here marks the submission as spam. |
altcha |
The solved ALTCHA challenge. |
h-captcha-response |
hCaptcha token, when that provider is switched on. |
cf-turnstile-response |
Cloudflare Turnstile token. |
g-recaptcha-response |
Google reCAPTCHA token. |
mcaptcha__token |
mCaptcha token. |
Values the submitter controls are validated: addresses must parse, free text is stripped of line breaks and capped. An invalid value falls back to the form's own setting instead of being used. A routing rule always wins over these fields — a rule is a server-side decision, the field travels in a request anyone can craft.
File uploads
Send the request as multipart/form-data and every file part is stored, attached to the notification mail and downloadable from the dashboard. The field name is yours to choose; several files may share one name.
| Limit | Default | Environment variable |
|---|---|---|
| Size per file | 10 MB |
UPLOAD_MAX_FILE_SIZE_MB |
| Files per submission | 5 |
UPLOAD_MAX_FILES |
The limits are enforced while the request is being read, so an oversized part is aborted mid-stream and answered with 413 rather than buffered first. Check size and count in the browser as well — that turns a failed request into a helpful message.
Captcha
ALTCHA is a proof-of-work captcha: no cookies, no third party, no puzzle for the visitor — the browser spends a moment of computing time, and that is exactly what makes bulk submission expensive for a bot. Fetch a challenge, solve it, send the solution back in the altcha field.
Order matters: deliver the token first, then switch the requirement on in the form settings. The other way round every submission is rejected with 403. The client script does the whole dance for you — see its own page.
Self-hosted? ALTCHA_HMAC_KEY has to be set in the instance, otherwise the challenge endpoint answers 404 and the form setting has no effect.
After the submission
For a browser form post the server answers 303 and sends the visitor onwards. Four candidates are checked in this order: a routing rule, the redirect field, the form's configured URL, and finally our hosted thank-you page.
Every target is validated against the form's allowed domains before it is used. A rejected target is logged and the next candidate takes over — without that check the endpoint would be an open redirect on our own domain.
Errors of this endpoint
| Code | Cause |
|---|---|
404 Missing access key |
Neither in the URL nor in the body. |
404 Unknown access key |
No form has this key. |
403 This form is blocked |
The form was disabled — see the dashboard. |
403 Origin not permitted |
The request came from a domain that is not allowed. |
403 Captcha verification failed |
Token missing, wrong or already used. |
429 |
Per-form, per-IP or monthly limit reached. |
413 |
An upload was larger than the configured maximum. |
A submission that ran into a timeout may still have been stored. Never retry it blindly — you would create a duplicate. Read requests are safe to repeat.