# Opt-In Forms API

> Create, configure, and embed opt-in forms with the Broadcast API: manage fields, double opt-in flows, redirect URLs, and tag assignment from your code.

Source: https://sendbroadcast.net/docs/api-opt-in-forms

The Opt-In Forms API provides full CRUD (Create, Read, Update, Delete) operations for managing subscription forms. You can create, list, view, update, and delete opt-in forms programmatically, including their form blocks (text, input fields, and buttons).

For building and embedding forms from the dashboard, see [Opt-In Forms](https://sendbroadcast.net/docs/opt-in-forms). A/B variants are the one capability that exists only here and has no dashboard equivalent.

## Required Permissions

All endpoints require authentication via an API token with appropriate permissions:

- **Read Permission**: Required for GET endpoints (list, show)
- **Write Permission**: Required for POST, PATCH, DELETE endpoints

## Opt-In Form Object

The opt-in form object contains the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | The unique identifier of the form |
| `identifier` | string (UUID) | A unique UUID for the form, used for embedding |
| `label` | string | The internal name of the form (required) |
| `form_type` | string | Type of form: `web` or `embed` |
| `widget_type` | string | Widget display type (e.g., popup, slide-in, inline) |
| `enabled` | boolean | Whether the form is active (default: true) |
| `allowed_embedding_domains` | array | List of domains where the form can be embedded |
| `theme_settings` | object | Theme customization (colors, typography, layout, effects) |
| `automation_settings` | object | Automation config, see [Automation Settings](#automation-settings) below for the full set of supported keys. |
| `confirmation_email_template_id` | integer | ID of the confirmation email template used when double opt-in is enabled. Must belong to the same channel. |
| `welcome_email_template_id` | integer | ID of the welcome email template sent after a subscriber confirms (if welcome emails are enabled). Must belong to the same channel. |
| `security_settings` | object | Security config (honeypot, rate limiting, Turnstile) |
| `trigger_settings` | object | Display trigger config (timing, scroll, exit intent) |
| `widget_settings` | object | Widget-specific settings |
| `analytics` | object | Analytics data (views_count, unique_views_count, submissions_count, conversion_rate) |
| `is_variant` | boolean | Whether this form is an A/B test variant |
| `variant_name` | string | Name of the variant (if applicable) |
| `variant_weight` | integer | Traffic weight for A/B testing (0-100) |
| `variants_count` | integer | Number of variants for this form |
| `opt_in_form_blocks` | array | Form blocks for the initial view |
| `opt_in_post_submission_blocks` | array | Form blocks for the post-submission view |
| `embed_url` | string | URL to embed the form via JavaScript |
| `created_at` | datetime | When the form was created |
| `updated_at` | datetime | When the form was last updated |

## Automation Settings

The `automation_settings` object accepts the following keys. All are optional; unspecified keys retain their previous value when updating.

| Key | Type | Description |
| --- | --- | --- |
| `tag_list` | string | Comma-separated list of tags applied to subscribers who submit this form. |
| `sequence_ids` | array<integer> | IDs of sequences a confirmed subscriber is enrolled in. |
| `send_welcome_email` | boolean | Whether to send a welcome email after the subscriber confirms (uses `welcome_email_template_id`). |
| `double_opt_in` | boolean | Whether to require email confirmation before activating the subscriber. |
| `include_unsubscribe_link_in_confirmation` | boolean | Whether the confirmation email includes an unsubscribe link in the footer. Defaults to `true`. |
| `confirmation_redirect_url` | string | If set, subscribers are redirected to this URL after confirming instead of seeing Broadcast's confirmation page. Must be `http://` or `https://`. Only applies to opt-in-form-flow confirmations. See [Confirmation Pages](https://sendbroadcast.net/docs/confirmation-pages#redirect-after-confirmation). |

## Form Block Object

Each form block contains the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | The unique identifier of the block |
| `block_type` | string | Type of block (see Block Types below) |
| `form_view` | string | `initial_view` or `post_submission_view` |
| `position` | integer | Order of the block in the form |
| `text_value` | string | Text content (for `text` block type) |
| `heading_level` | string | Heading level (h1-h6) for text blocks |
| `input_field_label` | string | Label for input field |
| `input_field_placeholder` | string | Placeholder text for input field |
| `input_field_value_type` | string | Type of data: `email`, `first_name`, `last_name`, or `name` |
| `custom_field_key` | string | Key under which the value is stored on `subscriber.custom_data`. Used by `custom_field` blocks and by `input_field` blocks with `input_field_value_type: "custom"`. Lowercase letters, numbers, and underscores only; must start with a letter. |
| `field_required` | boolean | Whether the field is required |
| `field_validation` | string | Validation rules for the field |
| `multiline` | boolean | When `true`, a `custom_field` block (or `input_field` with `input_field_value_type: "custom"`) renders as a `` instead of a single-line `<input>`. Useful for free-text answers like feedback or "why did you sign up?". Default: `false`.</td> </tr> <tr> <td class="border border-gray-300 p-2">`button_label`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Label for button (default: "Sign Up")</td> </tr> <tr> <td class="border border-gray-300 p-2">`checkbox_label`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Label for checkbox</td> </tr> <tr> <td class="border border-gray-300 p-2">`checkbox_field_name`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Field name for checkbox value</td> </tr> <tr> <td class="border border-gray-300 p-2">`checkbox_required`</td> <td class="border border-gray-300 p-2">boolean</td> <td class="border border-gray-300 p-2">Whether checkbox must be checked</td> </tr> <tr> <td class="border border-gray-300 p-2">`dropdown_label`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Label for dropdown</td> </tr> <tr> <td class="border border-gray-300 p-2">`dropdown_field_name`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Field name for dropdown value</td> </tr> <tr> <td class="border border-gray-300 p-2">`dropdown_placeholder`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Placeholder text for dropdown</td> </tr> <tr> <td class="border border-gray-300 p-2">`dropdown_options`</td> <td class="border border-gray-300 p-2">array</td> <td class="border border-gray-300 p-2">Array of options for dropdown</td> </tr> <tr> <td class="border border-gray-300 p-2">`radio_label`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Label for radio button group</td> </tr> <tr> <td class="border border-gray-300 p-2">`radio_field_name`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Field name for radio value</td> </tr> <tr> <td class="border border-gray-300 p-2">`radio_options`</td> <td class="border border-gray-300 p-2">array</td> <td class="border border-gray-300 p-2">Array of options for radio buttons</td> </tr> <tr> <td class="border border-gray-300 p-2">`image_url`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">URL of the image. Accepts any publicly reachable URL. The dashboard editor can also fill this field by uploading a new file or picking from the channel's File Asset library: both write the resulting public URL into this attribute, so API consumers don't need to handle uploads themselves.</td> </tr> <tr> <td class="border border-gray-300 p-2">`image_alt`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Alt text for the image</td> </tr> <tr> <td class="border border-gray-300 p-2">`image_alignment`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Image alignment (left, center, right)</td> </tr> <tr> <td class="border border-gray-300 p-2">`image_width`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Image width</td> </tr> <tr> <td class="border border-gray-300 p-2">`divider_color`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Color of the divider</td> </tr> <tr> <td class="border border-gray-300 p-2">`divider_style`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Style of divider (solid, dashed, dotted)</td> </tr> <tr> <td class="border border-gray-300 p-2">`divider_width`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Width/thickness of the divider</td> </tr> <tr> <td class="border border-gray-300 p-2">`spacer_height`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Height of the spacer block</td> </tr> <tr> <td class="border border-gray-300 p-2">`custom_css`</td> <td class="border border-gray-300 p-2">string</td> <td class="border border-gray-300 p-2">Custom CSS styling for the block</td> </tr> </tbody> </table> ### Block Types - `text` - Display text or headings - `heading` - Heading text (h1-h6, controlled by `heading_level`) - `input_field` - Collect subscriber data (email, first_name, last_name, name, or a custom field via `input_field_value_type: "custom"` + `custom_field_key`) - `custom_field` - Free-text answer stored on `subscriber.custom_data[custom_field_key]`. Renders as `<input type="text">` by default, or as `<textarea>` when `multiline: true`. - `button` - Submit button - `checkbox` - Checkbox for consent or preferences. Stores `"true"` on `subscriber.custom_data[checkbox_field_name]` when checked; absent when unchecked. - `dropdown` - Dropdown select field - `radio_group` - Radio button group - `image` - Display an image. Set `image_url` to any publicly reachable URL. The dashboard editor additionally lets builders upload a file or pick from the channel's File Asset library: both options write the resulting public URL into `image_url`, so the API shape is the same regardless of how the image got there. - `divider` - Horizontal divider line - `spacer` - Vertical spacing ### Submission Limits When subscribers submit an opt-in form, individual `subscriber.custom_data` values are capped at **2048 bytes** per key. Submissions containing an oversized value are rejected with a clear per-field error and no subscriber is created. This applies to all blocks that write to `custom_data` (`custom_field`, `checkbox`, `dropdown`, `radio_group`, and `input_field` with `input_field_value_type: "custom"`). ## List Opt-In Forms
```
GET /api/v1/opt_in_forms
```

 Retrieve a list of all opt-in forms for the authenticated broadcast channel, ordered alphabetically by label. ### Query Parameters - `filter` (optional): Search forms by label - `page` (optional): Page number for pagination - `limit` (optional): Maximum number of forms to return (default: 250) ### Request
```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/opt_in_forms
```

 ### Response
```json
{
  "opt_in_forms": [
    {
      "id": 1,
      "identifier": "550e8400-e29b-41d4-a716-446655440000",
      "label": "Newsletter Signup",
      "form_type": "web",
      "enabled": true,
      "allowed_embedding_domains": [],
      "opt_in_form_blocks": [
        {
          "id": 1,
          "block_type": "text",
          "form_view": "initial_view",
          "position": 1,
          "text_value": "Subscribe to our newsletter",
          "input_field_label": null,
          "input_field_placeholder": null,
          "input_field_value_type": null,
          "button_label": null,
          "custom_css": null
        }
      ],
      "opt_in_post_submission_blocks": [],
      "created_at": "2024-01-01T12:00:00Z",
      "updated_at": "2024-01-01T12:00:00Z"
    }
  ],
  "pagination": {
    "total": 1,
    "count": 1,
    "from": 1,
    "to": 1,
    "current": 1,
    "total_pages": 1
  }
}
```

 ## Get Opt-In Form
```
GET /api/v1/opt_in_forms/:id
```

 Retrieve details of a specific opt-in form including all its blocks. ### Request
```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/opt_in_forms/123
```

 ### Response GET responses include the full set of block attributes: every field listed in the [Form Block Object](#form-block-object) table is returned, not just the text/input/button essentials. Block-type-specific fields (`dropdown_options`, `radio_options`, `checkbox_label`, `image_url`, etc.) are present even when set to `null` for blocks that don't use them, so clients can safely round-trip the response back through `PATCH` without losing data.
```json
{
  "id": 123,
  "identifier": "550e8400-e29b-41d4-a716-446655440000",
  "label": "Newsletter Signup",
  "form_type": "web",
  "enabled": true,
  "allowed_embedding_domains": ["example.com", "blog.example.com"],
  "opt_in_form_blocks": [
    {
      "id": 1,
      "block_type": "text",
      "form_view": "initial_view",
      "position": 1,
      "text_value": "Join our newsletter!",
      "heading_level": null,
      "input_field_label": null,
      "input_field_placeholder": null,
      "input_field_value_type": null,
      "button_label": null,
      "custom_field_key": null,
      "field_required": false,
      "field_validation": null,
      "multiline": false,
      "checkbox_label": null,
      "checkbox_field_name": null,
      "checkbox_required": false,
      "dropdown_label": null,
      "dropdown_field_name": null,
      "dropdown_placeholder": null,
      "dropdown_options": null,
      "radio_label": null,
      "radio_field_name": null,
      "radio_options": null,
      "image_url": null,
      "image_alt": null,
      "image_alignment": "center",
      "image_width": "100%",
      "divider_color": "#E5E7EB",
      "divider_style": "solid",
      "divider_width": "100%",
      "spacer_height": "24px",
      "custom_css": null,
      "style_settings": {}
    },
    {
      "id": 2,
      "block_type": "input_field",
      "form_view": "initial_view",
      "position": 2,
      "input_field_label": "Email Address",
      "input_field_placeholder": "you@example.com",
      "input_field_value_type": "email",
      "field_required": true
    },
    {
      "id": 3,
      "block_type": "dropdown",
      "form_view": "initial_view",
      "position": 3,
      "dropdown_label": "Current email tool",
      "dropdown_field_name": "current_email_tool",
      "dropdown_placeholder": "Select your current tool",
      "dropdown_options": ["Mailchimp", "Beehiiv", "ConvertKit / Kit", "Other / None"],
      "field_required": true
    },
    {
      "id": 4,
      "block_type": "custom_field",
      "form_view": "initial_view",
      "position": 4,
      "custom_field_key": "why_hosted",
      "input_field_label": "Why do you want the hosted version?",
      "input_field_placeholder": "A sentence or two is plenty",
      "field_required": true,
      "multiline": true
    },
    {
      "id": 5,
      "block_type": "button",
      "form_view": "initial_view",
      "position": 5,
      "button_label": "Subscribe Now"
    }
  ],
  "opt_in_post_submission_blocks": [
    {
      "id": 6,
      "block_type": "text",
      "form_view": "post_submission_view",
      "position": 1,
      "text_value": "Thanks for subscribing!"
    }
  ],
  "created_at": "2024-01-01T12:00:00Z",
  "updated_at": "2024-01-01T12:00:00Z"
}
```

 > Note: blocks #2-#6 above are abbreviated for readability: the API > always returns every field in the Form Block Object table. Fields > not relevant to a block's `block_type` come back as `null` or their > default value. ## Create Opt-In Form
```
POST /api/v1/opt_in_forms
```

 Create a new opt-in form with blocks. ### Parameters - `label` (required): Internal name of the form - `form_type` (optional): `web` (default) or `embed` - `enabled` (optional): Whether the form is active (default: true) - `allowed_embedding_domains` (optional): Array of domains where the form can be embedded - `opt_in_form_blocks_attributes` (optional): Array of form blocks for the initial view ### Request
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "opt_in_form": {
      "label": "Newsletter Signup",
      "form_type": "web",
      "enabled": true,
      "allowed_embedding_domains": ["example.com"],
      "opt_in_form_blocks_attributes": [
        {
          "block_type": "text",
          "text_value": "Subscribe to our newsletter!",
          "position": 1
        },
        {
          "block_type": "input_field",
          "input_field_label": "Email",
          "input_field_value_type": "email",
          "input_field_placeholder": "you@example.com",
          "position": 2
        },
        {
          "block_type": "button",
          "button_label": "Sign Up",
          "position": 3
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/opt_in_forms
```

 ### Response
```json
{
  "id": 123,
  "identifier": "550e8400-e29b-41d4-a716-446655440000",
  "label": "Newsletter Signup",
  "form_type": "web",
  "enabled": true,
  "allowed_embedding_domains": ["example.com"],
  "opt_in_form_blocks": [
    {
      "id": 1,
      "block_type": "text",
      "form_view": "initial_view",
      "position": 1,
      "text_value": "Subscribe to our newsletter!",
      "input_field_label": null,
      "input_field_placeholder": null,
      "input_field_value_type": null,
      "button_label": null,
      "custom_css": null
    },
    {
      "id": 2,
      "block_type": "input_field",
      "form_view": "initial_view",
      "position": 2,
      "text_value": null,
      "input_field_label": "Email",
      "input_field_placeholder": "you@example.com",
      "input_field_value_type": "email",
      "button_label": null,
      "custom_css": null
    },
    {
      "id": 3,
      "block_type": "button",
      "form_view": "initial_view",
      "position": 3,
      "text_value": null,
      "input_field_label": null,
      "input_field_placeholder": null,
      "input_field_value_type": null,
      "button_label": "Sign Up",
      "custom_css": null
    }
  ],
  "opt_in_post_submission_blocks": [],
  "created_at": "2024-01-01T12:00:00Z",
  "updated_at": "2024-01-01T12:00:00Z"
}
```

 ## Update Opt-In Form
```
PATCH /api/v1/opt_in_forms/:id
```

 Update an existing opt-in form. ### Parameters All parameters are optional. Only include the fields you want to update: - `label`: Internal name of the form - `form_type`: `web` or `embed` - `enabled`: Whether the form is active - `allowed_embedding_domains`: Array of domains - `opt_in_form_blocks_attributes`: Array of form blocks (include `id` to update, `_destroy: true` to delete) ### Request
```bash
curl -X PATCH \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "opt_in_form": {
      "label": "Updated Form Name",
      "enabled": false,
      "opt_in_form_blocks_attributes": [
        {
          "id": 1,
          "text_value": "Updated headline text"
        },
        {
          "id": 2,
          "_destroy": true
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/opt_in_forms/123
```

 ### Response Returns the updated opt-in form object. ## Delete Opt-In Form
```
DELETE /api/v1/opt_in_forms/:id
```

 Delete an opt-in form and all its blocks. ### Request
```bash
curl -X DELETE \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/opt_in_forms/123
```

 ### Response
```json
{
  "message": "Opt-in form deleted successfully"
}
```

 ## Get Form Analytics
```
GET /api/v1/opt_in_forms/:id/analytics
```

 Retrieve analytics data for a specific opt-in form. ### Query Parameters - `start_date` (optional): First day of the period, inclusive. ISO 8601 date, `YYYY-MM-DD` (default: 30 days ago) - `end_date` (optional): Last day of the period, inclusive. ISO 8601 date, `YYYY-MM-DD` (default: today) Both dates are inclusive, so `start_date=2026-01-01&end_date=2026-01-31` covers the whole of January. `end_date` must not fall before `start_date`. A value that is not an ISO 8601 date returns `400 Bad Request` rather than being guessed at, see <a href="/docs/api-authentication">API Authentication</a> for why slash-formatted dates are refused. The error names the parameter and echoes what arrived, so a stray space or a templating slip is visible from the response alone:
```json
{
  "error": "start_date must be an ISO 8601 date (YYYY-MM-DD), got \"01/02/2026\". Slash-formatted dates are ambiguous (01/02/2026 names a different month depending on the locale) so they are rejected rather than guessed at.",
  "param": "start_date",
  "received": "01/02/2026",
  "expected": "YYYY-MM-DD"
}
```

 ### Request
```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "http://your-domain.com/api/v1/opt_in_forms/123/analytics?start_date=2024-01-01&end_date=2024-01-31"
```

 ### Response
```json
{
  "form_id": 123,
  "identifier": "550e8400-e29b-41d4-a716-446655440000",
  "period": {
    "start_date": "2024-01-01",
    "end_date": "2024-01-31"
  },
  "totals": {
    "views": 1500,
    "unique_views": 1200,
    "submissions": 150,
    "conversion_rate": 12.5
  },
  "daily": [
    {
      "date": "2024-01-01",
      "views": 50,
      "unique_views": 45,
      "submissions": 5
    }
  ],
  "variants": [
    {
      "id": 124,
      "name": "Variant A",
      "weight": 50,
      "views": 750,
      "submissions": 80,
      "conversion_rate": 10.67
    }
  ]
}
```

 ## Create A/B Test Variant
```
POST /api/v1/opt_in_forms/:id/variants
```

 Create a new A/B test variant from an existing form. ### Parameters - `name` (optional): Name for the variant (default: "Variant N") - `weight` (optional): Traffic weight for the variant (default: 50) ### Request
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Variant B - New Headline",
    "weight": 50
  }' \
  http://your-domain.com/api/v1/opt_in_forms/123/variants
```

 ### Response Returns the created variant form object with status 201. ## Duplicate Form
```
POST /api/v1/opt_in_forms/:id/duplicate
```

 Create a copy of an existing form. ### Parameters - `label` (optional): Label for the new form (default: "Original Label (Copy)") ### Request
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Newsletter Signup v2"
  }' \
  http://your-domain.com/api/v1/opt_in_forms/123/duplicate
```

 ### Response Returns the duplicated form object with status 201. ## Error Responses If the request is invalid, the response will include an appropriate HTTP status code and error message: ### 401 Unauthorized
```json
{
  "error": "Unauthorized"
}
```

 This error occurs when: - No authorization header is provided - The token is invalid or expired - The token doesn't have the required permissions ### 404 Not Found
```json
{
  "error": "Opt-in form not found"
}
```

 This error occurs when: - The form ID doesn't exist - The form belongs to a different broadcast channel ### 422 Unprocessable Entity
```json
{
  "error": "Label can't be blank"
}
```

 This error occurs when: - Required fields are missing - Validation fails for any reason ## Usage Example: Creating a Complete Form Here's an example of creating a complete opt-in form with all block types:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "opt_in_form": {
      "label": "Lead Capture Form",
      "form_type": "embed",
      "enabled": true,
      "allowed_embedding_domains": ["example.com", "blog.example.com"],
      "opt_in_form_blocks_attributes": [
        {
          "block_type": "text",
          "text_value": "Get our weekly newsletter!",
          "position": 1
        },
        {
          "block_type": "input_field",
          "input_field_label": "First Name",
          "input_field_value_type": "first_name",
          "input_field_placeholder": "John",
          "position": 2
        },
        {
          "block_type": "input_field",
          "input_field_label": "Email Address",
          "input_field_value_type": "email",
          "input_field_placeholder": "john@example.com",
          "position": 3
        },
        {
          "block_type": "button",
          "button_label": "Subscribe",
          "position": 4
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/opt_in_forms
```

 |
