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"] }'| config | Rules | |
|---|---|---|
| version | 1 | |
| fields | 1 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_label | 1 to 40 characters | |
| success | {kind: message, message} or {kind: redirect, url} | Messages up to 300 characters. Redirects must be https. |
| template | minimal, card or dark | The 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.
| Request | Body or query | Returns |
|---|---|---|
| GET /v1/form-templates | none | The three starting configs. |
| GET, POST /v1/forms | name, config, list_ids, tags, double_opt_in, allowed_origins | Forms; POST returns 201. |
| GET, PATCH, DELETE /v1/forms/{id} | any of the above plus status | The form, or 204. |
| GET /v1/forms/{id}/submissions | limit (default 50), before | data: 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 } } }'| config | Values | |
|---|---|---|
| heading, body | Heading 1 to 120 characters, body up to 400 | |
| position | center, bottom_right, bottom_left | |
| trigger | {kind: time, seconds: 0 to 300}, {kind: scroll, percent: 1 to 100} or {kind: exit_intent} | |
| targeting | include 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. |
| Request | Body | Returns |
|---|---|---|
| GET, POST /v1/popups | name, form_id, config, status | Popups; POST returns 201. |
| GET, PATCH, DELETE /v1/popups/{id} | name, config, status | The popup, or 204. |
| GET /v1/popups/{id}/stats | none | impressions, 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.
| Request | Body | Returns |
|---|---|---|
| GET /v1/public/forms/{id} | none | The form id, name and config. Cached 30 seconds. |
| POST /v1/public/forms/{id}/submit | fields, website (honeypot), popup_id, page_url | status (subscribed or pending_confirmation), success, submission_id, contact_id. |
| POST /v1/public/forms/confirm | token | Confirms a double opt in from the email link. |
| GET /v1/public/popups/{id} | none | The popup config with its form. Live popups only. |
| POST /v1/public/popups/{id}/events | {type: impression | dismiss} | 204. |
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
| Code | Status | Meaning and fix |
|---|---|---|
| invalid_form_config, invalid_popup_config | 400 | The config breaks a rule above. The message and fix name it. |
| invalid_origin | 400 | Use scheme and host, such as https://example.com. At most 20. |
| invalid_status, invalid_name, invalid_tags, list_not_found | 400 | Form status is active or paused; popup status is draft, live or paused. |
| origin_not_allowed | 403 | Add the website's origin to allowed_origins. |
| unknown_form_field | 400 | Create the custom field first, or use a core key. |
| field_required, field_invalid, invalid_email | 400 | The submission does not match the form's fields. |
| rate_limited | 429 | Too many signups from one address. Wait as the fix says. |
| confirmation_invalid | 404 | The confirmation link expired or was used. |
| invalid_event | 400 | Popup events are impression or dismiss. |