Forms API
Capture subscribers through public form endpoints.
Capture subscribers through public form endpoints.
Endpoint
| Method | Path |
|---|---|
POST | /forms/:formId |
Supported content types
application/jsonapplication/x-www-form-urlencodedmultipart/form-data
Behavior
- The form endpoint must exist and be active — an unknown form id returns
404and an inactive form returns403 - Honeypot field is checked to block spam
- Field requirements are validated from form config (a failure returns
400) - Rate limiting is applied by IP
- The request body is capped at 100 KB; a larger body returns
400 - Optional redirect is supported after submission
Example request
curl -X POST "https://<deployment>.convex.site/forms/<formId>" \
-H "Content-Type: application/json" \
-d '{
"email": "lead@example.com",
"firstName": "Avery",
"company": "Acme"
}'
Typical responses
Success (HTTP 200). The form endpoint routes through the public token endpoint
shell, so the body carries a top-level ok and nests message under data:
{ "ok": true, "data": { "message": "Form submitted successfully" } }
If a redirect URL is configured for the form, a successful submission returns
302 to that URL instead of this JSON body.
Double opt-in / pending confirmation
When the topic the form subscribes to requires confirmation, the contact is not added until they confirm by email. The submission still returns HTTP 200, but with a distinct body flagging that a confirmation email was sent:
{
"ok": true,
"data": {
"message": "Please check your email to confirm your subscription",
"confirmationRequired": true
}
}
If a redirect URL is configured, this outcome redirects to it with
?confirmation=pending appended, so the success page can tailor its copy.
Double opt-in is the union of the form-level doubleOptIn toggle and the
topic's requireDoubleOptIn setting — enabling either one forces the
pending-confirmation flow. See Topics for how to enable it on
the topic.
Validation failure
Validation failure returns HTTP 400 with the standard error envelope. The
category is invalid_input; the machine-readable reason rides in data:
{
"error": {
"category": "invalid_input",
"message": "Email is required",
"data": { "reason": "validation_error" }
}
}