Form Ingestion¶
POST /f/{endpoint_id} accepts a form submission for a registered endpoint and
stores it. It is the URL that goes in the action attribute of an HTML form.
<form action="https://forms.example.com/f/contact-form" method="POST">
<input type="email" name="email" required />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
This route is public and stays public¶
It cannot require a header a browser form has no way to send. No management credential is read here, and one sent anyway is ignored rather than forwarded anywhere.
Public does not mean unbounded. The route is rate limited per source address and
per endpoint, and an attempt over either limit is refused with
429 rate_limit_exceeded. See Rate limiting.
Content types¶
application/x-www-form-urlencoded and multipart/form-data are both accepted,
so a plain HTML <form> works unchanged. Anything else is rejected with
415 unsupported_media_type.
File uploads are not supported. A multipart part carrying a file is rejected
with 422 file_upload_not_supported rather than silently dropped, so you always
know a value did not make it.
Repeated field names¶
Checkbox groups and multi-selects submit the same name several times. Every value is preserved, in order, both in the response count and in storage:
<input type="checkbox" name="topics" value="billing" checked />
<input type="checkbox" name="topics" value="api" checked />
is stored as {"topics": ["billing", "api"]}. No submitted value is discarded.
Every field is stored as a list, even when only one value arrived, so a webhook
receiver never has to guess whether a field is single or multi valued.
What a successful submission returns¶
{
"submission_id": "sub_48984534f33749c49a88de2d59400dce",
"endpoint_id": "contact-form",
"received_at": "2026-08-24T14:34:27.651841Z",
"field_count": 3,
"idempotent_replay": false,
"delivery": { "queued": true }
}
The status is 202 Accepted and deliberately not 201 Created: the submission
is stored and any delivery it owes is queued, but that delivery has not happened
yet.
Submitted values are not echoed back, because the client already has them.
delivery.queued says whether a webhook delivery is owed for this submission. No
outbound request is made during the form request, so the response says nothing
about whether your destination is reachable. That is the worker's business, and a
destination being down can no longer affect whether a form is accepted. See
Webhook delivery.
Endpoints that do not accept the submission¶
| Situation | Response | Stored |
|---|---|---|
| Endpoint ID is not well formed | 404 invalid_endpoint_id |
nothing |
| No endpoint with that ID exists | 404 endpoint_not_found |
nothing |
Endpoint exists but is_active is false |
409 endpoint_inactive |
nothing |
Disabling an endpoint takes effect on the very next submission. Nothing caches the endpoint, so there is no window in which a disabled endpoint still accepts a form. See Endpoint management.
Request limits¶
Every limit is configurable, and every one is enforced before anything is stored.
| Limit | Setting | Default | Refusal |
|---|---|---|---|
| Request body size | FORMS_MAX_BODY_BYTES |
262144 |
413 request_body_too_large |
| Number of name/value pairs | FORMS_MAX_FIELDS |
100 |
422 too_many_fields |
| Field name length | FORMS_MAX_FIELD_NAME_LENGTH |
128 |
422 field_name_too_long |
| Field value length | FORMS_MAX_FIELD_VALUE_LENGTH |
16384 |
422 field_value_too_long |
A repeated field name counts once per submitted value, so three checked boxes
under one name are three of your FORMS_MAX_FIELDS allowance.
Two content rules are not configurable:
- A field name may not be empty or contain control characters
(
422 invalid_field_name). Newlines inside a textarea value are fine. - A field value may not contain a null byte (
422 invalid_field_value). - A submission with no fields at all is refused with
422 empty_submission.
The body size cap runs in ASGI middleware, before the route and before the form
parser, so an oversized body is refused without being buffered. A request that
declares an oversized Content-Length is refused before a single body byte is
read, and a chunked request that omits Content-Length is cut off as soon as the
running total crosses the limit.
Retrying safely¶
If a client never sees the response it cannot tell whether the submission landed.
Send an Idempotency-Key header and the retry becomes safe. See
Idempotency.
Related¶
- Submissions API reference for the compact route contract
- Errors for every code this route can return
- Rate limiting for what counts as an attempt