Forms and popups

Collect signups into your audience from any website with a form, an optional popup that shows it, and one first party script.

Forms

A form turns a submission into a contact: it adds the contact (or updates it), applies the form's tags, adds it to the form's list_ids and records the submission. With double_opt_in, no contact exists until the person clicks the link in a confirmation email. Start from a template with GET /v1/form-templates: newsletter, waitlist and product-updates.

curl -X POST https://api.runearc.com/v1/forms \
  -H "Authorization: Bearer $RUNEARC_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Footer signup",
        "config": { "version": 1,
          "fields": [
            { "key": "email", "label": "Email", "type": "email", "required": true },
            { "key": "first_name", "label": "First name", "type": "text" }
          ],
          "button_label": "Subscribe",
          "success": { "kind": "message", "message": "Thanks, you are subscribed." },
          "template": "minimal",
          "brand": { "color": "#0007cd", "radius": 2 } },
        "list_ids": ["lst_..."], "tags": ["newsletter"],
        "double_opt_in": false, "allowed_origins": ["https://acme.com"] }'
configRules
version1
fields1 to 12. The first must be {key: email, type: email, required: true}.Types: email, text, textarea, select, checkbox. Keys are lowercase letters, digits and underscores, at most 40, not website, popup_id or page_url. Labels 1 to 80 characters. A select needs 1 to 20 options.
button_label1 to 40 characters
success{kind: message, message} or {kind: redirect, url}Messages up to 300 characters. Redirects must be https.
templateminimal, card or darkThe look inside the embed.
brand{color: #rrggbb, radius: 0 to 16}

Other form keys: status (active or paused; a paused form answers 404 to the script and accepts no submissions), allowed_origins (up to 20 origins such as https://acme.com; when set, a submit from another origin gets origin_not_allowed), and list_ids and tags (up to 20 tags). Every field key other than email, first_name and last_name must already exist as a custom field, or creating the form fails with unknown_form_field.

RequestBody or queryReturns
GET /v1/form-templatesnoneThe three starting configs.
GET, POST /v1/formsname, config, list_ids, tags, double_opt_in, allowed_originsForms; POST returns 201.
GET, PATCH, DELETE /v1/forms/{id}any of the above plus statusThe form, or 204.
GET /v1/forms/{id}/submissionslimit (default 50), beforedata: id, email, contact_id, status, source_kind, origin, page_url, popup_id, created_at.

The embed script

runearc-embed.js has no dependencies, sets no cookies and loads nothing else. Put a div with data-runearc-form where the form should appear and load the script once. The form renders inside a shadow root so page CSS cannot break it, and a broken form never throws on the host page. data-api names the API origin; without it the script uses the origin it was loaded from. runearc forms embed frm_... prints this snippet.

<div data-runearc-form="frm_..."></div>
<script src="https://runearc.com/runearc-embed.js" data-api="https://api.runearc.com" defer></script>

<!-- A popup: the same script tag, with the popup id -->
<script src="https://runearc.com/runearc-embed.js" data-api="https://api.runearc.com" data-popup="pop_..." defer></script>

The script also exposes window.Runearc.mount(element, formId) and window.Runearc.popup(popupId) for single page apps. A honeypot field named website is included; bots that fill it get the same success answer and no contact.

Popups

A popup shows a form on a trigger. It has a form_id, a status of draft, live or paused, and a config. Only a live popup is served.

curl -X POST https://api.runearc.com/v1/popups \
  -H "Authorization: Bearer $RUNEARC_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Exit offer", "form_id": "frm_...", "status": "live",
        "config": { "version": 1, "heading": "Before you go", "body": "Get product updates.",
          "position": "center", "trigger": { "kind": "exit_intent" },
          "targeting": { "include": [{ "match": "starts_with", "value": "/blog" }], "exclude": [] },
          "frequency": { "days_between": 7, "hide_after_conversion": true } } }'
configValues
heading, bodyHeading 1 to 120 characters, body up to 400
positioncenter, bottom_right, bottom_left
trigger{kind: time, seconds: 0 to 300}, {kind: scroll, percent: 1 to 100} or {kind: exit_intent}
targetinginclude and exclude: up to 20 rules each, {match: equals | starts_with | ends_with | contains, value}Matched against the page path plus query, case sensitive. Exclude wins. An empty include list means every page.
frequency{days_between: 0 to 365, hide_after_conversion}Remembered in first party localStorage. 0 shows on every page view.
RequestBodyReturns
GET, POST /v1/popupsname, form_id, config, statusPopups; POST returns 201.
GET, PATCH, DELETE /v1/popups/{id}name, config, statusThe popup, or 204.
GET /v1/popups/{id}/statsnoneimpressions, dismissals, conversions, conversion_rate (conversions over impressions, 0 with none).

Public endpoints

The script calls these without a key; they are safe to call from a browser. Submissions are rate limited per client address.

RequestBodyReturns
GET /v1/public/forms/{id}noneThe form id, name and config. Cached 30 seconds.
POST /v1/public/forms/{id}/submitfields, website (honeypot), popup_id, page_urlstatus (subscribed or pending_confirmation), success, submission_id, contact_id.
POST /v1/public/forms/confirmtokenConfirms a double opt in from the email link.
GET /v1/public/popups/{id}noneThe popup config with its form. Live popups only.
POST /v1/public/popups/{id}/events{type: impression | dismiss}204.
Spam and suppressed addresses get the same subscribed answer as real ones, so a form cannot be used to probe your list.

CLI and MCP

runearc forms create "Footer signup" --template newsletter --double-opt-in
runearc forms list
runearc forms submissions frm_...
runearc forms embed frm_...
runearc popups list
runearc popups live pop_...
runearc popups stats pop_...

MCP tools: create_form, list_forms, list_form_submissions, create_popup and popup_stats. Landing pages that use a form are on Landing pages.

Errors

CodeStatusMeaning and fix
invalid_form_config, invalid_popup_config400The config breaks a rule above. The message and fix name it.
invalid_origin400Use scheme and host, such as https://example.com. At most 20.
invalid_status, invalid_name, invalid_tags, list_not_found400Form status is active or paused; popup status is draft, live or paused.
origin_not_allowed403Add the website's origin to allowed_origins.
unknown_form_field400Create the custom field first, or use a core key.
field_required, field_invalid, invalid_email400The submission does not match the form's fields.
rate_limited429Too many signups from one address. Wait as the fix says.
confirmation_invalid404The confirmation link expired or was used.
invalid_event400Popup events are impression or dismiss.

Something unclear or wrong? The API answers every error with a fix, and so should these docs. Back to runearc.com