Forms
Forms capture contact data. Every form has its own hosted page, an embed script for your website, and a public submit endpoint, all rendered with the same design.
Showing a form to visitors
| Way | Address | Use it when |
|---|---|---|
| Hosted page | GET /f/{formId} | You want a link to share: in an email, a message, a button. The page carries the form’s title and description. |
| Embed script | <div data-campaignos-form="FORM_ID"></div> followed by <script async src="https://your-domain.com/api/forms/FORM_ID/embed"></script> | The form should sit inside your own page. It renders in a shadow root, so your site’s styles cannot break it, while it takes your site’s font and text colour. |
| Plain HTML | Copied from the builder’s Embed tab | You want full control of the markup and will style it yourself. It is a copy: paste it again after changing the form. |
Both the hosted page and the script always show the saved form, so later edits appear without touching your website. The rendered form validates inline in the visitor’s language (the browser’s own messages), shows a loading state while submitting, then the success message or a redirect. A hidden spam-trap field is included; a submission that fills it is accepted but discarded.
Appearance
settings on the form controls how it looks: title and description above the fields, accentColor (a hex colour for the button, selected choices and focus), submitButtonText, and after submitting either successMessage or redirectUrl. honeypotField renames the spam-trap field (default cos_website).
List Forms
GET /api/workspaces/{workspaceId}/forms
Auth: Session required
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 50 | Items per page |
Response:
{
"data": [
{
"id": "frm_abc123",
"name": "Newsletter Signup",
"fields": [...],
"doubleOptIn": false,
"settings": {},
"_count": { "submissions": 500 },
"createdAt": "2026-01-15T10:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 10, "totalPages": 1 }
}Create Form
POST /api/workspaces/{workspaceId}/forms
Auth: Session required
Request Body:
{
"name": "Newsletter Signup",
"fields": [
{ "name": "email", "type": "email", "label": "Email Address", "required": true },
{ "name": "firstName", "type": "text", "label": "First Name", "required": false },
{ "name": "interests", "type": "select", "label": "Interests", "options": ["tech", "marketing", "design"] }
],
"settings": {
"redirectUrl": "https://example.com/thank-you",
"successMessage": "Thanks for signing up!"
},
"doubleOptIn": true
}Field Definition
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Field identifier (used as form key) |
type | string | Yes | Stored type: text, email, tel, number, select, checkbox or textarea |
builderType | string | No | How the field is shown; see below |
label | string | Yes | Display label |
required | boolean | No | Whether field is required |
options | string[] | No | Choices for dropdown, single-choice and multiple-choice fields |
placeholder | string | No | Placeholder text |
helpText | string | No | Shown under the field |
defaultValue | string | No | Pre-filled value, or the pre-selected choice |
width | string | No | full (default) or half; two half fields share a row |
validation | object | No | minLength, maxLength, pattern (a regular expression), max (rating scale) |
builderType picks the control: TEXT, TEXTAREA, NUMBER, EMAIL, PHONE, URL, DATE, SELECT (dropdown), MULTISELECT (checkboxes, submitted as an array), RADIO (one choice), RATING (a 1–5 scale, or 0–10 with validation.max: 10), BOOLEAN (a single checkbox), HIDDEN and SUBMIT_BUTTON (its label becomes the button text). Without it, the stored type decides.
A HIDDEN field named locale that has no value is filled with the visitor’s browser language when the form is rendered, so new contacts get a locale.
Update Form
PUT /api/workspaces/{workspaceId}/forms/{formId}
Auth: Session required (MEMBER or above)
Request Body: Any subset of name, fields, settings, doubleOptIn, actions, progressiveFields. Only the keys you send are changed; omitted keys keep their saved value. fields replaces the whole field list and field names must be unique.
Submit Form (Public)
POST /api/forms/{formId}/submit
Public endpoint for form submissions. CORS-enabled.
Auth: None required
Rate Limit: 10 requests per minute per IP + form combination
Request Body: Key-value pairs matching form field names.
{
"email": "jane@example.com",
"firstName": "Jane",
"interests": "marketing"
}Response (201):
{
"success": true,
"submissionId": "sub_abc123"
}What Happens on Submit
- Validates required fields
- Creates or updates contact by email
- Sets consent to
OPTED_IN(unless double opt-in is enabled) - Fires a
form.submittedevent on the contact - Triggers active automations whose trigger matches that event, in the background so the response is not delayed
Errors:
404— Form not found422— Validation failed, withdetailssuch as["email is required", "score is invalid"]; the rendered form maps each entry back to its field429— Rate limited
A request with Content-Type: application/x-www-form-urlencoded (a plain HTML form without the script) is answered with a 303 redirect to redirectUrl when one is set.
Embed script and hosted page (public)
GET /api/forms/{formId}/embed
Returns the JavaScript that draws the form inside the element [data-campaignos-form="{formId}"] (or right before the script tag when there is none). Cached for one minute. CORS-enabled.
GET /f/{formId}
The hosted page. Its language follows the visitor’s Accept-Language, which selects the built-in validation messages.