---
title: Lead from a web form
description: Turn a form submission into a lead from your own server, with the visitor's details in the quick fields, an owner and a lead source, in REST or GraphQL.
canonical: https://developers.coevera.com/recipes/lead-from-web-form/
section: recipes
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Lead from a web form

**Answer:** On your server, never in the browser, create a lead with name, an owner_id (or a unit_id), the visitor's details in quick_contact_name, quick_email, quick_account_name and quick_phone, the message in description and a lead_source_id looked up in Data. No contact or account is created. Encode the e-mail when you search by it: a raw + becomes a space.

Jordan Reyes of Contoso Freight fills in the demo-request form on Northwind's website. Your form
handler turns the submission into a **lead** for the sales rep, with the channel it came from,
and the rep qualifies it later (see [Lead lifecycle](/recipes/lead-lifecycle/)).

The form posts to your own server, and the server calls the API. Never call the API from the
page itself: the application's credentials would be in every visitor's browser.

## Before you start

|                    |                                                                                                                                                                                                     |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permissions        | **Full Access** to **Leads** on the **Records** tab, and **Read Only Access** to **Common Lists** on the **Administration** tab for the lead source. The owner lookup also reads users (`Clients`). |
| `validation-level` | The default (`0`, full validation).                                                                                                                                                                 |
| Batch behaviour    | No batches: every write is one record.                                                                                                                                                              |
| Context            | Region and space ID come from the context bar. Credentials come from `$COEVERA_API_KEY` and `$COEVERA_API_SECRET`.                                                                                  |

The programs need `curl` and `jq`, Node.js 18 or later, or Python 3.9 or later with `requests`.
[Authentication](/get-started/authentication/) shows where the credentials come from, and
[Permissions and scopes](/get-started/permissions-and-scopes/) where to set the permissions.

The operations this recipe calls, in the API reference: REST
[`GET /entities/Clients`](/reference/rest/clients/#list),
[`GET /entities/Data`](/reference/rest/data/#list),
[`POST /entities/Leads`](/reference/rest/leads/#create),
[`GET /entities/Leads`](/reference/rest/leads/#list); GraphQL
[`ClientRepository.getByCriteria`](/reference/graphql/client/#q-getByCriteria),
[`DataRepository.getByCriteria`](/reference/graphql/data/#q-getByCriteria),
[`createLead`](/reference/graphql/lead/#m-createLead),
[`LeadRepository.getById`](/reference/graphql/lead/#q-getById).

## Set up a helper

Every step below uses this setup, and the submission as your form handler receives it. Export the
credentials first:

```bash
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>"   # never commit it
```

**REST · cURL**

```bash
COEVERA_BASE="https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"

# The submission, as your form handler receives it.
FORM_NAME="Jordan Reyes"
FORM_EMAIL="jordan.reyes+demo@example.com"
FORM_COMPANY="Contoso Freight"
FORM_PHONE="+1 206 555 0115"
FORM_MESSAGE="We run 40 trucks and want dashboards for fuel use."
```

**REST · TypeScript**

```ts
const base = `https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

async function coevera(path: string, init: { method?: string; body?: unknown } = {}) {
  const res = await fetch(`${base}${path}`, {
    method: init.method ?? "GET",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`HTTP ${res.status} ${body.name}: ${String(body.message).trim()}`);
  return body;
}

// The submission, as your form handler receives it.
const form = {
  name: "Jordan Reyes",
  email: "jordan.reyes+demo@example.com",
  company: "Contoso Freight",
  phone: "+1 206 555 0115",
  message: "We run 40 trucks and want dashboards for fuel use.",
};
```

**REST · Python**

```python
BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def coevera(method, path, **kwargs):
    response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
    body = response.json()
    if not response.ok:
        sys.exit(f"HTTP {response.status_code} {body.get('name')}: {str(body.get('message')).strip()}")
    return body


# The submission, as your form handler receives it.
form = {
    "name": "Jordan Reyes",
    "email": "jordan.reyes+demo@example.com",
    "company": "Contoso Freight",
    "phone": "+1 206 555 0115",
    "message": "We run 40 trucks and want dashboards for fuel use.",
}
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"

# gql: POST the GraphQL request read from stdin. GraphQL reports errors in the body, often
# with HTTP 200, so fail on those too.
gql() {
  local response
  response=$(curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
  if jq -e '.errors' <<<"$response" >/dev/null; then
    echo "$response" >&2
    return 1
  fi
  echo "$response"
}

# The submission, as your form handler receives it.
FORM_NAME="Jordan Reyes"
FORM_EMAIL="jordan.reyes+demo@example.com"
FORM_COMPANY="Contoso Freight"
FORM_PHONE="+1 206 555 0115"
FORM_MESSAGE="We run 40 trucks and want dashboards for fuel use."
```

**GraphQL · TypeScript**

```ts
const endpoint = `https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

// gql: one GraphQL request. GraphQL reports errors in the body, often with HTTP 200.
async function gql(query: string, variables: object = {}) {
  const res = await fetch(endpoint, {
    method: "POST",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  const body = await res.json();
  if (!res.ok || body.errors) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.errors ?? body)}`);
  return body.data;
}

// The submission, as your form handler receives it.
const form = {
  name: "Jordan Reyes",
  email: "jordan.reyes+demo@example.com",
  company: "Contoso Freight",
  phone: "+1 206 555 0115",
  message: "We run 40 trucks and want dashboards for fuel use.",
};
```

**GraphQL · Python**

```python
ENDPOINT = (
    "https://{region}.api.coevera.com/api/v100/app/space/"
    "{space_id}/graphql/public"
)
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def gql(query, variables=None):
    """One GraphQL request. GraphQL reports errors in the body, often with HTTP 200."""
    response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
    body = response.json()
    if not response.ok or body.get("errors"):
        sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
    return body["data"]


# The submission, as your form handler receives it.
form = {
    "name": "Jordan Reyes",
    "email": "jordan.reyes+demo@example.com",
    "company": "Contoso Freight",
    "phone": "+1 206 555 0115",
    "message": "We run 40 trucks and want dashboards for fuel use.",
}
```

## 1. Find the owner

Decide who gets web leads; here, one sales rep, looked up by e-mail.

**REST · cURL**

```bash
# Who gets web leads: here one sales rep.
owner=$(curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Clients" \
  --data-urlencode "filter[email]=dana.whitfield@example.com" \
  -d load-only=id,name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
echo "$owner"
OWNER_ID=$(jq -er '.data[0].id' <<<"$owner")
```

**REST · TypeScript**

```ts
// Who gets web leads: here one sales rep.
const owners = await coevera(
  `/entities/Clients?${new URLSearchParams({ "filter[email]": "dana.whitfield@example.com", "load-only": "id,name" })}`,
);
const ownerId: string | undefined = owners.data[0]?.id;
if (!ownerId) throw new Error("No user with that e-mail in this space.");
console.log("Owner:", owners.data[0].name, ownerId);
```

**REST · Python**

```python
# Who gets web leads: here one sales rep.
owners = coevera(
    "GET",
    "/entities/Clients",
    params={"filter[email]": "dana.whitfield@example.com", "load-only": "id,name"},
)
if not owners["data"]:
    sys.exit("No user with that e-mail in this space.")
owner_id = owners["data"][0]["id"]
print("Owner:", owners["data"][0]["name"], owner_id)
```

**GraphQL · cURL**

```bash
# Who gets web leads: here one sales rep.
owner=$(jq -n '{
  query: "{ entities { client { getByCriteria(first: 1, filter: { email: { eq: [\"dana.whitfield@example.com\"] } }) { edges { node { id name } } } } } }"
}' | gql)
echo "$owner"
OWNER_ID=$(jq -er '.data.entities.client.getByCriteria.edges[0].node.id' <<<"$owner")
```

**GraphQL · TypeScript**

```ts
// Who gets web leads: here one sales rep.
const owners = await gql(`{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}`);
const owner = owners.entities.client.getByCriteria.edges[0]?.node;
if (!owner) throw new Error("No user with that e-mail in this space.");
console.log("Owner:", owner.name, owner.id);
```

**GraphQL · Python**

```python
# Who gets web leads: here one sales rep.
owners = gql("""
{
  entities {
    client {
      getByCriteria(first: 1, filter: { email: { eq: ["dana.whitfield@example.com"] } }) {
        edges { node { id name } }
      }
    }
  }
}
""")["entities"]["client"]["getByCriteria"]["edges"]
if not owners:
    sys.exit("No user with that e-mail in this space.")
owner = owners[0]["node"]
print("Owner:", owner["name"], owner["id"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwZDAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwZDAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "0d000000-0000-4000-a000-000000000001",
      "name": "Dana Whitfield"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "client": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "0d000000-0000-4000-a000-000000000001",
                "name": "Dana Whitfield"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 2. Find the lead source

The lead source is a dropdown option, stored in `Data`: look its `id` up by the option's name.
The program looks up Direct traffic, one of options such as Organic search, Paid ad and Referral;
yours may differ. An option name can repeat in another dropdown, so check the one you get.

**REST · cURL**

```bash
# The lead source is a dropdown option: look its id up by the option's name.
source=$(curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Data" \
  --data-urlencode "filter[option_name]=Direct traffic" \
  -d load-only=id,option_name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
echo "$source"
LEAD_SOURCE_ID=$(jq -er '.data[0].id' <<<"$source")
```

**REST · TypeScript**

```ts
// The lead source is a dropdown option: look its id up by the option's name.
const sources = await coevera(`/entities/Data?${new URLSearchParams({ "filter[option_name]": "Direct traffic", "load-only": "id,option_name" })}`);
const leadSourceId: string | undefined = sources.data[0]?.id;
if (!leadSourceId) throw new Error("No lead source with that name.");
console.log("Lead source:", sources.data[0].option_name, leadSourceId);
```

**REST · Python**

```python
# The lead source is a dropdown option: look its id up by the option's name.
sources = coevera(
    "GET", "/entities/Data", params={"filter[option_name]": "Direct traffic", "load-only": "id,option_name"}
)
if not sources["data"]:
    sys.exit("No lead source with that name.")
lead_source_id = sources["data"][0]["id"]
print("Lead source:", sources["data"][0]["option_name"], lead_source_id)
```

**GraphQL · cURL**

```bash
# The lead source is a dropdown option: look its id up by the option's name.
source=$(jq -n '{
  query: "{ entities { data { getByCriteria(first: 1, filter: { optionName: { eq: [\"Direct traffic\"] } }) { edges { node { id optionName } } } } } }"
}' | gql)
echo "$source"
LEAD_SOURCE_ID=$(jq -er '.data.entities.data.getByCriteria.edges[0].node.id' <<<"$source")
```

**GraphQL · TypeScript**

```ts
// The lead source is a dropdown option: look its id up by the option's name.
const sources = await gql(`{
  entities {
    data {
      getByCriteria(first: 1, filter: { optionName: { eq: ["Direct traffic"] } }) {
        edges { node { id optionName } }
      }
    }
  }
}`);
const source = sources.entities.data.getByCriteria.edges[0]?.node;
if (!source) throw new Error("No lead source with that name.");
console.log("Lead source:", source.optionName, source.id);
```

**GraphQL · Python**

```python
# The lead source is a dropdown option: look its id up by the option's name.
sources = gql("""
{
  entities {
    data {
      getByCriteria(first: 1, filter: { optionName: { eq: ["Direct traffic"] } }) {
        edges { node { id optionName } }
      }
    }
  }
}
""")["entities"]["data"]["getByCriteria"]["edges"]
if not sources:
    sys.exit("No lead source with that name.")
source = sources[0]["node"]
print("Lead source:", source["optionName"], source["id"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIxOTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIxOTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "19000000-0000-4000-a000-000000000001",
      "option_name": "Direct traffic"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "data": {
        "getByCriteria": {
          "edges": [
            {
              "node": {
                "id": "00000000-0000-4000-a000-000000000001",
                "optionName": "Direct traffic"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 3. Create the lead

A lead needs a `name` and a sales unit. Send `owner_id`, and the lead takes the owner's unit; or
send only `unit_id`, and the lead waits unassigned in that unit. Without either, the create
answers 422 ([Leads reference](/reference/rest/leads/#create)).

The visitor's details go into the quick fields, `quick_contact_name`, `quick_email`,
`quick_account_name` and `quick_phone`, and the message into `description`. The quick fields are
text on the lead: no contact or account is created.

**REST · cURL**

```bash
# The visitor's details go into the quick fields: no contact or account is created yet. jq
# builds the body, so quotes in the visitor's message cannot break the JSON.
lead=$(jq -n --arg owner "$OWNER_ID" --arg source "$LEAD_SOURCE_ID" \
  --arg name "$FORM_NAME" --arg email "$FORM_EMAIL" --arg company "$FORM_COMPANY" \
  --arg phone "$FORM_PHONE" --arg message "$FORM_MESSAGE" \
  '{
    name: "\($company): demo request", owner_id: $owner, lead_source_id: $source,
    quick_contact_name: $name, quick_email: $email, quick_account_name: $company,
    quick_phone: $phone, description: $message
  }' |
  curl -sS --fail-with-body "$COEVERA_BASE/entities/Leads" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
echo "$lead"
LEAD_ID=$(jq -er '.data.id' <<<"$lead")
```

**REST · TypeScript**

```ts
// The visitor's details go into the quick fields: no contact or account is created yet.
const lead = await coevera("/entities/Leads", {
  method: "POST",
  body: {
    name: `${form.company}: demo request`,
    owner_id: ownerId,
    lead_source_id: leadSourceId,
    quick_contact_name: form.name,
    quick_email: form.email,
    quick_account_name: form.company,
    quick_phone: form.phone,
    description: form.message,
  },
});
const leadId: string = lead.data.id;
console.log("Lead:", leadId);
```

**REST · Python**

```python
# The visitor's details go into the quick fields: no contact or account is created yet.
lead = coevera(
    "POST",
    "/entities/Leads",
    json={
        "name": f"{form['company']}: demo request",
        "owner_id": owner_id,
        "lead_source_id": lead_source_id,
        "quick_contact_name": form["name"],
        "quick_email": form["email"],
        "quick_account_name": form["company"],
        "quick_phone": form["phone"],
        "description": form["message"],
    },
)
lead_id = lead["data"]["id"]
print("Lead:", lead_id)
```

**GraphQL · cURL**

```bash
# The visitor's details go into the quick fields: no contact or account is created yet.
lead=$(jq -n --arg owner "$OWNER_ID" --arg source "$LEAD_SOURCE_ID" \
  --arg name "$FORM_NAME" --arg email "$FORM_EMAIL" --arg company "$FORM_COMPANY" \
  --arg phone "$FORM_PHONE" --arg message "$FORM_MESSAGE" '{
  query: "mutation CreateLead($input: CreateLeadInput!) { createLead(input: $input) { lead { id } } }",
  variables: {
    input: {
      name: "\($company): demo request", ownerId: $owner, leadSourceId: $source,
      quickContactName: $name, quickEmail: $email, quickAccountName: $company,
      quickPhone: $phone, description: $message
    }
  }
}' | gql)
echo "$lead"
LEAD_ID=$(jq -er '.data.createLead.lead.id' <<<"$lead")
```

**GraphQL · TypeScript**

```ts
// The visitor's details go into the quick fields: no contact or account is created yet.
const created = await gql(
  `mutation CreateLead($input: CreateLeadInput!) {
    createLead(input: $input) { lead { id } }
  }`,
  {
    input: {
      name: `${form.company}: demo request`,
      ownerId: owner.id,
      leadSourceId: source.id,
      quickContactName: form.name,
      quickEmail: form.email,
      quickAccountName: form.company,
      quickPhone: form.phone,
      description: form.message,
    },
  },
);
const leadId: string = created.createLead.lead.id;
console.log("Lead:", leadId);
```

**GraphQL · Python**

```python
# The visitor's details go into the quick fields: no contact or account is created yet.
created = gql(
    """
    mutation CreateLead($input: CreateLeadInput!) {
      createLead(input: $input) { lead { id } }
    }
    """,
    {
        "input": {
            "name": f"{form['company']}: demo request",
            "ownerId": owner["id"],
            "leadSourceId": source["id"],
            "quickContactName": form["name"],
            "quickEmail": form["email"],
            "quickAccountName": form["company"],
            "quickPhone": form["phone"],
            "description": form["message"],
        }
    },
)
lead_id = created["createLead"]["lead"]["id"]
print("Lead:", lead_id)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "08000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.000000+00:00",
    "created": "2026-10-15 09:00:00.000047+00:00",
    "currency_exchange_rates_list": null,
    "lead_source": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Data/19000000-0000-4000-a000-000000000001",
    "lead_type": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/LeadTypes/00000000-0000-4000-a000-000000000001",
    "owner": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Clients/0d000000-0000-4000-a000-000000000001",
    "reason_of_close": null,
    "step": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Steps/07000000-0000-4000-a000-000000000001",
    "unit": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/SalesUnits/05000000-0000-4000-a000-000000000001",
    "currency_exchange_rates_list_id": null,
    "description": "We run 40 trucks and want dashboards for fuel use.",
    "is_archived": false,
    "label_flag": 0,
    "last_ai_buying_center_applied": null,
    "lead_source_id": "19000000-0000-4000-a000-000000000001",
    "lead_type_id": "00000000-0000-4000-a000-000000000001",
    "name": "Contoso Freight: demo request",
    "owner_id": "0d000000-0000-4000-a000-000000000001",
    "quick_account_email": "",
    "quick_account_name": "Contoso Freight",
    "quick_account_phone": "",
    "quick_contact_name": "Jordan Reyes",
    "quick_email": "jordan.reyes+demo@example.com",
    "quick_phone": "+1 206 555 0115",
    "ranking": 50,
    "reason_of_close_description": "",
    "reason_of_close_id": null,
    "share_mode": 0,
    "step_id": "07000000-0000-4000-a000-000000000001",
    "table_name": "lead",
    "unit_id": "05000000-0000-4000-a000-000000000001",
    "revision": 1259,
    "primary_contact": null,
    "primary_account": null,
    "contact_relations": [],
    "account_relations": [],
    "status": 1,
    "sharing_units": [],
    "sharing_clients": [],
    "documents": [],
    "tags": [],
    "static_profiles": [],
    "qualify_date": null,
    "days_in_step": 0,
    "is_favorite": false,
    "lost_date": null,
    "formatted_name": "Contoso Freight: demo request",
    "scoring": null,
    "modified_by_user": null
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createLead": {
      "lead": {
        "id": "08000000-0000-4000-a000-000000000001"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## 4. Read it back

Read the lead with its lead source expanded. The `+` in the visitor's e-mail is kept: the
programs send it in a JSON body, where it needs no encoding.

**REST · cURL**

```bash
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Leads" \
  -d "filter[id]=$LEAD_ID" -d expand=lead_source \
  -d load-only=id,name,owner_id,status,quick_contact_name,quick_email,quick_account_name,quick_phone,description,lead_source.option_name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
```

**REST · TypeScript**

```ts
const readBack = await coevera(
  `/entities/Leads?${new URLSearchParams({
    "filter[id]": leadId,
    expand: "lead_source",
    "load-only": "id,name,owner_id,status,quick_contact_name,quick_email,quick_account_name,quick_phone,description,lead_source.option_name",
  })}`,
);
console.log(JSON.stringify(readBack.data[0], null, 2));
```

**REST · Python**

```python
read_back = coevera(
    "GET",
    "/entities/Leads",
    params={
        "filter[id]": lead_id,
        "expand": "lead_source",
        "load-only": "id,name,owner_id,status,quick_contact_name,quick_email,quick_account_name,quick_phone,"
        "description,lead_source.option_name",
    },
)
print(json.dumps(read_back["data"][0], indent=2))
```

**GraphQL · cURL**

```bash
jq -n --arg id "$LEAD_ID" '{
  query: "query ReadBack($id: ID!) { entities { lead { getById(entityId: $id) { id name status owner { name } quickContactName quickEmail quickAccountName quickPhone description leadSource { optionName } } } } }",
  variables: { id: $id }
}' | gql
```

**GraphQL · TypeScript**

```ts
const readBack = await gql(
  `query ReadBack($id: ID!) {
    entities {
      lead {
        getById(entityId: $id) {
          id name status
          owner { name }
          quickContactName quickEmail quickAccountName quickPhone description
          leadSource { optionName }
        }
      }
    }
  }`,
  { id: leadId },
);
console.log(JSON.stringify(readBack.entities.lead.getById, null, 2));
```

**GraphQL · Python**

```python
read_back = gql(
    """
    query ReadBack($id: ID!) {
      entities {
        lead {
          getById(entityId: $id) {
            id name status
            owner { name }
            quickContactName quickEmail quickAccountName quickPhone description
            leadSource { optionName }
          }
        }
      }
    }
    """,
    {"id": lead_id},
)
print(json.dumps(read_back["entities"]["lead"]["getById"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIwODAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwODAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "id": "08000000-0000-4000-a000-000000000001",
      "lead_source": {
        "option_name": "Direct traffic"
      },
      "description": "We run 40 trucks and want dashboards for fuel use.",
      "name": "Contoso Freight: demo request",
      "owner_id": "0d000000-0000-4000-a000-000000000001",
      "quick_account_name": "Contoso Freight",
      "quick_contact_name": "Jordan Reyes",
      "quick_email": "jordan.reyes+demo@example.com",
      "quick_phone": "+1 206 555 0115",
      "status": 1
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "lead": {
        "getById": {
          "id": "08000000-0000-4000-a000-000000000001",
          "name": "Contoso Freight: demo request",
          "status": "Open",
          "owner": {
            "name": "Dana Whitfield"
          },
          "quickContactName": "Jordan Reyes",
          "quickEmail": "jordan.reyes+demo@example.com",
          "quickAccountName": "Contoso Freight",
          "quickPhone": "+1 206 555 0115",
          "description": "We run 40 trucks and want dashboards for fuel use.",
          "leadSource": {
            "optionName": "Direct traffic"
          }
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.3 in the docs sandbox, de-identified._

## What to check

- Step 2 returns one option. An empty `data` means no option has that name.
- Step 3 answers **201** (GraphQL: 200) with the lead's `id`.
- Step 4 returns the lead open (`status: 1`, GraphQL `Open`), owned by the rep, with the quick
  fields, the message and the lead source.
- Each submission makes a new lead, so a visitor who sends the form twice gets two. To merge
  repeats, look for an open lead first with `filter[quick_email]` (GraphQL
  `quickEmail: { eq: [...] }`), and URL-encode the address: a raw `+` in a query string arrives
  as a space and matches nothing.

## Clean up

The program leaves the lead in your space. Delete it with `POST /entities/Leads/batch-delete`.

## Limits

- The program trusts the submission. Validate it, and protect the form from spam, before you
  call the API.
- Coevera also has its own online forms, which create leads without a server of yours; this recipe
  does not cover them.
- A lead becomes an opportunity only through GraphQL `qualifyLead`; see
  [Lead lifecycle](/recipes/lead-lifecycle/).
