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

# Connect a website

> Send your website's form enquiries into Marro as leads.

<Steps>
  <Step title="Create a Website form key">
    In Marro, open **Settings → Connections → API keys → New key → Website form**. Copy the key.
  </Step>

  <Step title="Store the key on your website's server">
    Save it as an environment variable, for example `MARRO_API_KEY`. In WordPress, define it in `wp-config.php`.
  </Step>

  <Step title="Send each enquiry from your server">
    Your form posts to your own website's server (a route handler, a server action or a PHP handler). The server sends the enquiry to Marro with the key, using the code below. `name` and `phone` are required.
  </Step>

  <Step title="Add your own fields">
    Send other details in `fields`, using the field keys shown in **Settings → Leads → Lead fields** or returned by [List lead fields](/api-reference/reference/list-fields).
  </Step>

  <Step title="Test it">
    Submit your form once and check that the lead appears in Marro, with the enquiry on its timeline.
  </Step>
</Steps>

<Warning>
  Never call the API from browser JavaScript. Anyone could read the key from the page.
</Warning>

[Capture a lead](/api-reference/leads/capture-lead): `POST /api/v1/leads/capture`

For website and landing-page forms. Adds the enquiry as a new lead, or, when the organization already has a lead with this phone number or email (matched ignoring case), fills in that lead instead: only fields it has empty are filled, and values that differ from what your team entered are kept and listed on the lead’s timeline. The owner, stage and pipeline of an existing lead are never changed, and an archived lead is not changed at all (the enquiry is noted on its timeline). A new lead starts in the starting stage and gets an owner from the organization’s assignment rules. Every capture is recorded on the lead’s timeline, naming the API key that sent it.

Server to server only: call it from your website’s server (a route handler, a server action, PHP), never from browser JavaScript, where the key would be visible to anyone. There is no CORS support on purpose.

Retries are safe: send an `Idempotency-Key` header (or an `externalId`), and a repeat within 24 hours returns the first result without capturing again.

Scope: `leads.capture` or `leads.write`.

### Code

<CodeGroup>
  ```typescript Next.js theme={null}
  // app/api/enquiry/route.ts on your website. It runs on the server, so the key stays secret.
  // Your form posts to /api/enquiry; this sends the enquiry on to the CRM.
  export async function POST(request: Request) {
    const form = await request.json();
    // One ID per submission. Send the same one again if you retry, and the CRM captures it once.
    const submissionId = form.submissionId ?? crypto.randomUUID();

    const response = await fetch("https://app.marro.si/api/v1/leads/capture", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CRM_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": submissionId,
      },
      body: JSON.stringify({
        name: form.name,
        phone: form.phone,
        email: form.email,
        source: "Website",
        fields: {
          "city": form.city,
          "interest": form.interest,
        },
        attribution: {
          pageUrl: form.pageUrl,
          referrer: form.referrer,
          formName: "Contact form",
        },
      }),
      signal: AbortSignal.timeout(10_000),
    });

    if (!response.ok) {
      // Keep the enquiry (a sheet, a database row, an email) and retry later with the same
      // submissionId, so no lead is lost while the CRM cannot be reached.
      console.error("CRM capture failed", response.status, await response.text());
    }
    return Response.json({ ok: true });
  }
  ```

  ```php PHP / WordPress theme={null}
  <?php
  // In wp-config.php (never in a theme file anyone can read, never in JavaScript):
  // define('CRM_API_KEY', 'mk_live_...');

  $response = wp_remote_post('https://app.marro.si/api/v1/leads/capture', [
      'timeout' => 10,
      'headers' => [
          'Authorization'   => 'Bearer ' . CRM_API_KEY,
          'Content-Type'    => 'application/json',
          // One ID per submission; reuse it if you retry and the CRM captures it once.
          'Idempotency-Key' => wp_generate_uuid4(),
      ],
      'body' => wp_json_encode([
          'name'   => sanitize_text_field($_POST['name'] ?? ''),
          'phone'  => sanitize_text_field($_POST['phone'] ?? ''),
          'email'  => sanitize_email($_POST['email'] ?? ''),
          'source' => 'Website',
          'fields' => [
              'city' => sanitize_text_field($_POST['city'] ?? ''),
              'interest' => sanitize_text_field($_POST['interest'] ?? ''),
          ],
          'attribution' => [
              'pageUrl'  => esc_url_raw(wp_get_referer() ?: ''),
              'formName' => 'Contact form',
          ],
      ]),
  ]);

  if (is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 300) {
      // Keep the enquiry (for example, email it to yourself) so no lead is lost.
      error_log('CRM capture failed: ' . (is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_body($response)));
  }
  ```

  ```bash cURL theme={null}
  curl -X POST "https://app.marro.si/api/v1/leads/capture" \
    -H "Authorization: Bearer $CRM_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{
    "name": "Priya Sharma",
    "phone": "+919812345678",
    "email": "priya@example.com",
    "source": "Website",
    "fields": {
      "city": "Hyderabad",
      "interest": "Premium plan"
    },
    "attribution": {
      "pageUrl": "https://www.example.com/contact",
      "formName": "Contact form"
    }
  }'
  ```
</CodeGroup>

### Request body

| Field | Type | Description |
| - | - | - |
| `name` (required) | string | The person’s name. |
| `phone` (required) | string | With the country code, e.g. +919812345678. Numbers without one are read as Indian. Used, with email, to find a lead the organization already has. |
| `email` | string | Also used to find an existing lead when the phone number does not match one. |
| `source` | string | The source’s name, e.g. "Website" (the default) or "Google Ads". It must be one of the organization’s active sources (matched ignoring case; list them with GET /api/v1/lead-sources). Any other name is recorded as "Website", so a website key cannot fill the organization’s source list. |
| `pipelineId` | integer | For a new lead, the pipeline it starts in (IDs from GET /api/v1/pipelines); it lands in that pipeline’s starting stage. Defaults to the organization’s default pipeline. Never moves an existing lead. |
| `notes` | string | Free text shown on the lead’s timeline with this enquiry, such as the visitor’s message. |
| `fields` | object | More details, by field key: the organization’s built-in and custom lead fields (GET /api/v1/lead-fields). Values are checked with the same rules as the CRM’s forms: dropdowns take one of their options (value or label), numbers, dates (YYYY-MM-DD) and yes/no fields are parsed. An unknown key is refused with the list of valid keys. On an existing lead only empty fields are filled. |
| `attribution` | object | Where the enquiry came from. Shown on the lead’s timeline. |
| `externalId` | string | Your own ID for this submission. A resend with the same externalId (per API key) within 24 hours returns the first result instead of capturing again. |

### Headers

| Header | Description |
| - | - |
| `Idempotency-Key` | Optional. A unique value per submission, such as a UUID; remembered per API key. A retry with the same key within 24 hours returns the first result, with `Idempotent-Replayed: true`; the same key with a different body is refused. |

### Responses

| Status | Meaning |
| - | - |
| 200 | The organization already had this lead; it was found and any empty fields filled in. `Location` points at it. |
| 201 | A new lead was created. `Location` points at it. |
| 400 | INVALID\_IDEMPOTENCY\_KEY: The Idempotency-Key header is longer than 200 characters or not plain ASCII. |
| 401 | UNAUTHORIZED: The key is missing, wrong, revoked or expired. |
| 403 | FORBIDDEN: The key lacks the scope, or the organization’s plan does not include it. |
| 409 | IDEMPOTENCY\_IN\_PROGRESS: A request with this Idempotency-Key is still being handled. Retry in a few seconds. Another request saved the same person at the same moment. Retry; it will find that lead. |
| 422 | VALIDATION\_FAILED: A value is missing or invalid, a key in `fields` is not one of the organization’s active fields (`details.validKeys` lists them), or a field the organization requires for a new lead is missing. This Idempotency-Key was used within 24 hours for a different body. `pipelineId` is not one of the organization’s pipelines. |
| 429 | RATE\_LIMITED: More than 600 requests in an hour with this key, or 3000 with all of the organization’s keys together. `Retry-After` says how many seconds to wait. |
| 500 | INTERNAL: Something failed on our side. Retry; if it persists, contact support quoting the `X-Request-Id` response header. |
| 503 | NOT\_READY: The organization has no stage for new leads, or no active user to own them. Retry after it is set up. |
