Forms API

Capture subscribers through public form endpoints.

Capture subscribers through public form endpoints.

Endpoint

MethodPath
POST/forms/:formId

Supported content types

  • application/json
  • application/x-www-form-urlencoded
  • multipart/form-data

Behavior

  • The form endpoint must exist and be active — an unknown form id returns 404 and an inactive form returns 403
  • 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" }
  }
}