
# Connecting a Website Form

A form endpoint turns a form you already have on your website into a lead source. You create the endpoint, add a short script to your page that posts the form's fields to it, send one test, map the fields, and submissions start creating and updating contacts.

## What you'll need

- `crm:forms:manage` in Karmaflow.ai — tenant administrators have it; it puts **Create Form Endpoint** on the Forms page.
- A way to add a `<script>` to the page that holds your form (or to your site builder's custom-code slot).
- Your form must collect an **email address** — it is how a submission is matched to a contact.

## Step 1 — Create the endpoint

1. Go to **CRM → Setup → Forms**. The page is titled **Form Endpoints**.
2. Click **+ Create Form Endpoint**.
3. Fill in **Form Endpoint Details**:
   - **Form Name** — how it appears in lists and on the contact's timeline ("Contact Us", "Pilot Request").
   - **Email Recipients** — comma-separated addresses that get an email for every live submission. Leave blank for none.
   - **Email Subject** — the subject of that notification.
   - **Allowed Origins (CORS)** — the sites allowed to post to this endpoint, as full origins (`https://www.example.com`), comma-separated. Leave `*` while you test; narrow it before you go live.
4. Click **Create Form Endpoint**.
5. The **Form Endpoint Created!** dialog shows your **API key** and **Endpoint URL**. **Copy the key now — it is shown once.** If you lose it, **Regenerate API Key** on the edit page issues a new one and retires the old.
6. Click **Configure Form** to open the endpoint's edit page.

<!-- SCREENSHOT: forms/endpoint-created.png — the "Form Endpoint Created!" dialog on the Demo tenant with the API key masked and the endpoint URL visible -->

## Step 2 — Post your form to it

On the edit page, the **Integration Guide** card has three tabs — **JavaScript**, **HTML Form** and **cURL** — with your endpoint URL already filled in. The **HTML Form** tab is the usual starting point:

```html
<form id="contactForm">
    <input type="email" name="email" required>
    <input type="text" name="name">
    <textarea name="message"></textarea>
    <button type="submit">Submit</button>
</form>

<script>
document.getElementById('contactForm')
  .addEventListener('submit', async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const data = Object.fromEntries(formData);
    const response = await fetch('https://omni.karmaflow.ai/api/forms/<your-workspace>/<form>', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-API-Key': 'YOUR_API_KEY'
        },
        body: JSON.stringify(data)
    });
    const result = await response.json();
    if (result.success) alert('Submitted!');
});
</script>
```

Keep your own fields and styling; what matters is that the page sends the fields as JSON (or URL-encoded) to the endpoint URL with the `X-API-Key` header. Field names are yours to choose — you map them in the next step.

Three things to know before you paste:

- **A plain `<form action="…">` will not work.** The API key travels in a request header, which a browser's native form post cannot send, so the endpoint answers `401`. The script is required.
- **The key is visible to anyone who views your page source.** That is inherent to posting from a browser. **Allowed Origins**, the rate limit and reCAPTCHA are what stop the key being useful anywhere but your site — set them (Step 5).
- **Up to 50 fields and 50 KB per submission.** HTML in values is stripped.

## Step 3 — Send a test

Submit your form once from the page (or run the **cURL** tab's command). Until fields are mapped the endpoint treats every submission as a **test**: it records the field names and values and creates no contact. The response says so:

```json
{ "success": true, "isTest": true,
  "message": "Test submission received. Please configure field mappings in the admin panel." }
```

Back on the edit page, **Detected Fields** switches from *Awaiting First Submission* to *N fields detected* and lists each field with the value you sent. Wrong field set? **Clear & Re-detect** and submit again. On the Forms list the endpoint's status moves from **Awaiting Test** to **Needs Mapping**.

## Step 4 — Map the fields

In **Field Mapping**, choose a destination for each detected field:

| Choose | To |
|---|---|
| **-- Skip this field --** | Keep the value in the submission record but write it nowhere on the contact |
| **Full Name → First Name + Last Name** | Split one name field into the two contact fields |
| A **standard field** — First Name, Last Name, Email, Phone, Company, LinkedIn ID, Years Experience | Write it to that contact field (phone numbers are normalised with your workspace's default country) |
| **Notes (adds as a note)** | Add the value as a note on the contact, prefixed with the form's name |
| A **custom field** | Write it to one of your contact custom fields |
| **+ Create New Custom Field** | Make a new custom field for it, right here |

One field **must** map to **Email** — **Save Field Mappings** refuses otherwise, because email is how a submission finds its contact.

Click **Save Field Mappings**. The endpoint is now **Active**, and the next submission is live. **Remap** clears the map if you need to start over.

<!-- SCREENSHOT: forms/field-mapping.png — the Field Mapping card on the Demo tenant with name, email, phone and message mapped, and the green "Save Field Mappings" button -->

## Step 5 — Lock it down

Back in **Form Details** on the edit page:

1. Replace `*` in **Allowed Origins (CORS)** with your site's origin(s). A post from any other origin is refused with `403 Origin not allowed`.
2. Set **Rate Limit (per minute)** — the default is 60 submissions a minute per endpoint; above it the endpoint answers `429` with a `Retry-After` header.
3. Click **Save Changes**.

For bot protection, open the **reCAPTCHA v3** card, tick **Enable reCAPTCHA v3**, paste your reCAPTCHA **secret key**, set the **Score Threshold** (Google suggests 0.5; higher is stricter) and **Save reCAPTCHA Settings**. Your page must then include the token Google issues as a `captchaToken` field (and the action name as `captchaAction`); a missing token is refused with `400`, a failing score with `403`.

AI spam scoring is on for every endpoint by default: each live submission is scored against the last ten, and anything over the threshold is stored as **Spam** and creates no contact — while the response still reads as a success, so a bot cannot tell.

## What a live submission does

1. Matches the contact by email. An existing contact is **updated** with the mapped fields; a new email becomes a **contact** in the *lead* lifecycle stage. A submitter whose address is on one of your own domains ([Internal Email Domains](/help/crm/email-capture#how-karmaflowai-reads-a-copy)) is stored with the outcome *skipped — internal domain* and no contact is created.
2. Writes *Submitted web form "Form Name" (contact created)* on the contact's **Activity Timeline**, with a **View submission** link.
3. Fires the **Form Endpoint Submission** workflow trigger for this endpoint — build an [orchestration](/help/orchestrations/building-workflows) on it and pick the form in the trigger's **Form Endpoint** select. Only that form's submissions start it.
4. Emails your **Email Recipients** a table of the submitted values.
5. If the Karmaflow.ai tracker is on the page and your form posts to the endpoint URL (or carries `data-kf-attribution`), the visitor's click id, campaign tags and GA4 session are lifted off the submission onto the contact, and a `generate_lead` event is sent when the [GA4 integration](/help/integrations/google-analytics-setup) is on.

## Reading submissions

**CRM → Setup → Forms → the eye icon** (or **View Submissions**) opens **Form Submissions** for the endpoint. Filter by status — **All · Test · Received · Processed · Failed · Spam** — and click **View** on a row for **Submission Details**: when it arrived, its status and **CRM Outcome** (*created contact*, *updated contact*, *skipped — internal domain*), the IP, user agent and referring page, the spam score, the **Submitted Data** as sent, the **Mapped Contact Data** as written, and an **Open** link to the contact.

The same submission is reachable from the contact's timeline row.

## Email Intake (optional)

Open the **Email Intake** card and click **Enable Email Intake**. The endpoint gets its own address, `form-<name>.<id>@sys.karmaflow.ai`. Mail sent or forwarded there is read by AI, the fields are extracted according to your field map and the result is processed as a normal submission. **AI Processing Instructions** lets you give hints ("the subject line holds the person's name"). Use it when a partner or an older system emails you leads instead of posting them.

## Troubleshooting

**`401 API key required` or `Invalid API key`.** The header is missing or the key is wrong. Check the header is literally `X-API-Key`, and that the key is the one shown at creation (or after the last **Regenerate API Key**). A native form post without script always lands here.

**`403 Origin not allowed`.** The page's origin is not in **Allowed Origins (CORS)**. Add the exact origin including scheme and any `www.`.

**`429 Rate limit exceeded`.** More than **Rate Limit (per minute)** submissions in a minute. Raise it on the edit page, or wait for `Retry-After`.

**The status stays "Awaiting Test".** No submission has reached the endpoint yet. Run the **cURL** tab's command to prove the endpoint works, then look at your page's network tab.

**Submissions arrive but no contact is created.** Open the submission: a *Test* status means the fields are not mapped yet (Step 4); *Spam* means the AI score crossed the threshold; *skipped — internal domain* means the submitter's domain is one of yours.

**My workflow runs for other forms.** Open the orchestration and check the trigger's **Form Endpoint** select names this form. A trigger with no form selected runs for every endpoint.

## Related

- [Forms Overview](/help/forms/overview)
- [Building Workflows](/help/orchestrations/building-workflows) — act on each submission
- [Google Analytics 4 workflows](/help/integrations/google-analytics-workflows) — attribution on submissions
- [Managing Contacts](/help/crm/contacts)
