> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyops.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Landing page forms

> Send a landing page's form, such as one built with Lovable, into RevyOps as a Form submission, with the API key kept off the page.

## What This Recipe Does

A visitor fills in the form on your landing page. The page sends the answers to a small backend function of its own, such as a Supabase edge function in a Lovable project, and the function logs them in RevyOps as a [Form submission](https://docs.revyops.com/data-model/activities.md#meeting-and-form-submission). A visitor RevyOps doesn't know yet becomes a new person, and a work email such as `ana@northwind.io` also creates the company **northwind.io**. A known person's filled-in fields stay as they are.

The key lives only in the function, as a secret. Put it in the page's own code and anyone who opens the page can read it in the browser. Use an Activities only key even there: it can log activities and nothing else, so a leaked key can't read, change or delete anything in the workspace.

> [!WARNING]
>
> Never call the RevyOps API from the page itself, and never paste a key into a Lovable prompt or a frontend file. Lovable publishes every frontend file, so a key there is public.

## Setup

**1. Generate an Activities only key**

In RevyOps, pick the workspace in the sidebar, open **Developer → Keys**, click **Generate key**, choose **Activities only API key** and name it after the page, such as **Lovable landing page**. Copy the `AC-` key from the reveal dialog: it is shown once.

**2. Store the key as a secret**

Add a secret named `REVYOPS_API_KEY`, with the key as its value, to the backend that runs the function. For a Supabase edge function, run `supabase secrets set REVYOPS_API_KEY=AC-...` with the Supabase CLI. The function reads the secret at run time, and the page never sees it.

**3. Add the backend function**

Create an edge function named `submit-form` with the code below. The property names must match the Form submission fields in your workspace, as listed under [Activities & Scoring](https://docs.revyops.com/user-guide/activities-and-scoring.md#meeting-and-form-submission): a name the workspace doesn't have refuses the whole submission.

**4. Call the function from the form**

Have the form's submit button call the function, for example `supabase.functions.invoke("submit-form", { body: { firstName, lastName, email, company, pageUrl: window.location.href } })`, then show your own thank-you message.

**5. Send a test**

Submit the form with an email RevyOps doesn't have. The new person appears under **People** with the Form submission on their timeline, and on the company's timeline when the email is a work email.

```ts supabase/functions/submit-form/index.ts theme={null}
const REVYOPS_URL = "https://app.revyops.com/api/public/activities/custom/";
const CORS = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: CORS });
  const form = await req.json();

  const response = await fetch(REVYOPS_URL, {
    method: "POST",
    headers: { "X-API-KEY": Deno.env.get("REVYOPS_API_KEY") ?? "", "Content-Type": "application/json" },
    body: JSON.stringify({
      activity_key: "revyops.form_submission",
      occured_at: new Date().toISOString(),
      properties: {
        "First name": form.firstName,
        "Last name": form.lastName,
        "Email": form.email,
        "Company name": form.company,
        "Page URL": form.pageUrl,
      },
    }),
  });

  // Logged for you, never shown to the visitor.
  if (!response.ok) console.error("RevyOps refused the submission", response.status, await response.text());
  return new Response(JSON.stringify({ ok: true }), { headers: { ...CORS, "Content-Type": "application/json" } });
});
```

## What Happens to Each Submission

| The form's email or phone | What RevyOps does |
| - | - |
| Belongs to nobody yet | Creates the person from the form's fields and logs the submission on them. A work email also creates the company, named by **Company name**. |
| Is a personal email, such as `ana@gmail.com` | Creates the person without a company. |
| Belongs to a known person | Logs the submission on them and fills only their empty fields. A form sent as Mallory by Anna leaves her name Anna. |
| Is missing, with no email or phone on the form | Refuses the submission with `400`. |

The function answers the visitor the same way every time, so nobody sees a RevyOps error. Check the function's logs in Supabase when a test doesn't show up: a `422` names the field or identifier RevyOps refused, and a `403` means the key is wrong or revoked.

**Create the person when not found** is on for Form submission. Switch it off under **Edit Activity** and a submission from someone new is refused instead of creating them. See [Create the person when not found](https://docs.revyops.com/data-model/activities.md#create-the-person-when-not-found).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.