# Forms API

Create and manage embeddable signup forms from code. Every response includes the
snippets, so an agent or a script can create a form and paste the tag into a
site in one pass.

## Endpoints

- `GET /v1/forms`
- `POST /v1/forms`
- `GET /v1/forms/{id}`
- `PATCH /v1/forms/{id}`
- `DELETE /v1/forms/{id}`
- `POST /v1/forms/{id}/publish`
- `POST /v1/forms/{id}/duplicate`
- `GET /v1/forms/{id}/embed`
- `GET /v1/forms/{id}/submissions`
- `GET /v1/form-themes`
- `POST /v1/form-themes`
- `PATCH /v1/form-themes/{id}`
- `DELETE /v1/form-themes/{id}`
- `POST /v1/form-themes/{id}/copy`

## Presets

`GET /v1/form-presets` lists the ready-made forms. Pass one as `preset` and it
becomes your starting point — anything else you send is laid over it, so
`{"preset": "welcome-popup", "confirmation": "none"}` gives you the popup with
your confirmation setting rather than its own.

`newsletter` · `inline-bar` · `welcome-popup` · `image-popup` · `two-step` ·
`sticky-bar` · `lead-capture` · `waitlist`

## Create a form

Omit `fields` and `preset` and you get the starter form: an email field and a
consent checkbox with GDPR-shaped wording already in it. Pass `new_list` to
create the audience at the same time, for when there isn't one yet.

```bash
curl -X POST https://emailbump.com/api/v1/forms \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Welcome popup", "preset": "welcome-popup", "new_list": "Subscribers" }'
```

```bash
curl -X POST https://emailbump.com/api/v1/forms \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Newsletter", "lists": ["LIST_ID"] }'
```

A fuller spec:

```json
{
  "name": "Footer newsletter",
  "slug": "newsletter",
  "kind": "inline",
  "lists": ["LIST_ID"],
  "confirmation": "double",
  "fields": [
    { "type": "email",   "key": "email",      "label": "Email", "required": true },
    { "type": "text",    "key": "first_name", "label": "First name" },
    { "type": "select",  "key": "plan",       "label": "Plan", "options": ["Free", "Pro"] },
    { "type": "consent", "key": "marketing_consent",
      "text": "Yes, email me news and offers. Unsubscribe any time." },
    { "type": "hidden",  "key": "source", "value": "{{utm_source}}" }
  ],
  "theme": "thm_yourbrand",
  "design": { "width": 320, "layout": "horizontal" },
  "on_submit": { "message": "Check your inbox to confirm.", "hide_form": true },
  "protection": { "captcha": true, "block_disposable": true }
}
```

New forms are created as `draft`. Publish when you're ready:

```bash
curl -X POST https://emailbump.com/api/v1/forms/frm_8f2c9a1b3d/publish \
  -H "Authorization: Bearer ebk_your_key"
```

## Field types

`email` `text` `textarea` `number` `date` `select` `checkboxes` `boolean`
`consent` `lists` `hidden` `heading` `paragraph` `divider` `image` `spacer`

`step` is a divider rather than a field: everything before the first one is step
one, everything after it step two, up to four. Its `text` is the button that ends
the step. Both steps ship in the markup, so a two-step form degrades into one
long form without JavaScript, and the whole thing submits once, at the end.

Every form needs exactly one `email` field, and it is always required. There is
no separate attribute registry to maintain — the field **is** the declaration,
and values are normalised (bool / number / date / string) before they merge into
the contact's attributes.

- `consent` needs `text`. That exact wording is stored with every submission and
  repeated in the confirmation email, so the record says what was agreed to.
- `lists` renders checkboxes bound to real lists, letting someone pick what they
  want. Ticking nothing subscribes them to nothing — the choice was offered.
- `hidden` fills `{{utm_source}}`-style tokens from the page's own URL.
  `{{page_url}}` and `{{referrer}}` also work.

## The id never changes

Three identifiers, three jobs:

| | Example | Changes? | Used for |
| --- | --- | --- | --- |
| **Form id** | `frm_8f2c9a1b3d` | never | every embed, every API call, the permanent URL |
| **Slug** | `newsletter` | any time | the vanity URL only, unique within your project |

Rename a form, restyle it, point it at a different list — the id stays put, so
the tag on your website never needs re-pasting. Renaming the slug changes
`/f/your-project/newsletter` and nothing else.

## Get the embed code

```bash
curl https://emailbump.com/api/v1/forms/frm_8f2c9a1b3d/embed \
  -H "Authorization: Bearer ebk_your_key"
```

Returns `script`, `iframe`, `html`, `react`, `vue`, `url` and `permanent_url`.
The one most people want:

```html
<script src="https://emailbump.com/f.js" data-form="frm_8f2c9a1b3d" async></script>
```

## Submissions

Every submission is stored — including the ones the anti-spam checks turned
away, because a form quietly rejecting thousands of attempts from one network is
only visible if the refusals are recorded next to the successes.

```bash
curl "https://emailbump.com/api/v1/forms/frm_8f2c9a1b3d/submissions?limit=50" \
  -H "Authorization: Bearer ebk_your_key"
```

Each row carries the consent snapshot, IP, referring page, and location, plus a
`status` of `pending_confirmation`, `confirmed`, `accepted` or `rejected`.
Filter with `?status=`.

## Themes

A theme is shared styling for a project. Forms store only their own overrides,
so changing a theme restyles every form on it — live, with no re-embedding.

```bash
curl -X PATCH https://emailbump.com/api/v1/form-themes/thm_yourbrand \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "tokens": { "accent": "#111827", "radius": 8, "font": "system" } }'
```

Themes are scoped to a project, like contacts, lists and templates. To reuse a
brand elsewhere, copy it — the copy is an independent snapshot that drifts
freely from the original:

```bash
curl -X POST https://emailbump.com/api/v1/form-themes/thm_yourbrand/copy \
  -H "Authorization: Bearer ebk_account_key" \
  -H "Content-Type: application/json" \
  -d '{ "to_project": "PROJECT_ID" }'
```

Copying names two projects, so it takes an **account-scoped** key; a project key
can only speak for the project it belongs to. The destination may be in any
workspace you're a member of.

## Targeting

`targeting` decides whether a page view qualifies; `trigger` decides when within
one that does.

```json
{
  "targeting": {
    "include_urls": [{ "match_type": "contains", "value": "/pricing" }],
    "exclude_urls": [{ "match_type": "starts_with", "value": "/checkout" }],
    "audience": "new_visitors",
    "show": "until_subscribed"
  },
  "trigger": { "exit_intent": true, "frequency_days": 30 }
}
```

`match_type` is `contains` (default), `exact`, `starts_with` or `ends_with`.
Exclusions are checked last and always win. `audience` is `everyone`,
`new_visitors` or `returning_visitors`; `show` is `until_subscribed`, `once` or
`always`. All of it is evaluated in the visitor's own browser, so clearing site
data starts them over.

## Stats

A form carries `views`, `submissions` and `contacts`. `views` counts times the
form was actually shown — reported by the embed when it becomes visible, not
when the markup was fetched, so a popup whose targeting or trigger never fires
is not counted.

## Dark mode

`design.color_scheme` is `auto` (the default), `light` or `dark`. On `auto`, any
colour still at its default gets a dark counterpart when the visitor prefers
dark; colours you set are kept in both schemes.

## Colours and URLs are checked, not passed through

Design colours must be a hex value, an `rgb()`/`rgba()` value, or `transparent`
— they end up inside a stylesheet we generate for your page. Redirect URLs must
be absolute `http://` or `https://`. Anything else is refused with an
explanation rather than silently dropped.
