> ## 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.

# Capture a lead

> 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`.



## OpenAPI

````yaml /openapi.json post /api/v1/leads/capture
openapi: 3.1.0
info:
  title: Marro API
  version: 1.0.0
  description: >-
    Server-to-server access to one organization’s data in Marro. Each key
    belongs to an organization and can use only the scopes it was given for
    modules the organization subscribes to. Keys go only in the Authorization
    header, over HTTPS, from a server: never in browser code or a URL. A key
    that was ever sent over plain HTTP or in a URL must be treated as leaked:
    revoke it and create a new one. The API sends no CORS headers.


    Every response carries `X-Request-Id`, to quote to support. Once the key is
    checked, responses also carry `RateLimit-Limit`, `RateLimit-Remaining` and
    `RateLimit-Reset` (seconds until the window resets) for whichever limit is
    closer to running out. An address that is not an endpoint answers 404
    `NOT_FOUND` in the same error shape.
servers:
  - url: https://app.marro.si
security: []
tags:
  - name: Leads
  - name: Reference
    description: 'How this organization is set up: fields, stages, sources and people.'
paths:
  /api/v1/leads/capture:
    post:
      tags:
        - Leads
      summary: Capture a lead
      description: >-
        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`.
      operationId: capture-lead
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            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.
          schema:
            type: string
            minLength: 1
            maxLength: 200
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: The person’s name.
                phone:
                  type: string
                  minLength: 6
                  maxLength: 32
                  description: >-
                    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:
                  description: >-
                    Also used to find an existing lead when the phone number
                    does not match one.
                  type: string
                  maxLength: 254
                source:
                  description: >-
                    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.
                  type: string
                  minLength: 1
                  maxLength: 60
                pipelineId:
                  description: >-
                    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.
                  type: integer
                  exclusiveMinimum: 0
                  maximum: 2147483647
                notes:
                  description: >-
                    Free text shown on the lead’s timeline with this enquiry,
                    such as the visitor’s message.
                  type: string
                  maxLength: 5000
                fields:
                  description: >-
                    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.
                  type: object
                  propertyNames:
                    type: string
                    maxLength: 64
                  additionalProperties:
                    anyOf:
                      - type: string
                      - type: number
                      - type: boolean
                      - type: array
                        items:
                          type: string
                      - type: 'null'
                attribution:
                  $ref: '#/components/schemas/CaptureAttribution'
                  description: Where the enquiry came from. Shown on the lead’s timeline.
                externalId:
                  description: >-
                    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.
                  type: string
                  minLength: 1
                  maxLength: 200
              required:
                - name
                - phone
              additionalProperties: false
            example:
              name: Priya Sharma
              phone: '+919812345678'
              email: priya@example.com
              source: Website
              notes: Wants to know the fee structure for 2027.
              fields:
                city: Hyderabad
                state: Telangana
                interest: Premium plan
                preferred_time: evening
              attribution:
                pageUrl: >-
                  https://www.example.com/pricing?utm_source=google&utm_medium=cpc
                pageTitle: Pricing
                referrer: https://www.google.com/
                utmSource: google
                utmMedium: cpc
                utmCampaign: mbbs-2027
                formName: Footer enquiry
              externalId: web-48213
      responses:
        '200':
          description: >-
            The organization already had this lead; it was found and any empty
            fields filled in. `Location` points at it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lead:
                    type: object
                    properties:
                      id:
                        type: integer
                        exclusiveMinimum: 0
                        maximum: 2147483647
                        description: The lead’s ID, new or existing.
                      created:
                        type: boolean
                        description: True when a new lead was created.
                      updated:
                        type: boolean
                        description: >-
                          True when an existing lead was found and at least one
                          empty field on it was filled.
                      archived:
                        description: >-
                          Present when the person matched an archived lead:
                          nothing on it was changed, the enquiry was noted on
                          its timeline, and there is no `Location`.
                        type: boolean
                        const: true
                    required:
                      - id
                      - created
                      - updated
                    additionalProperties: false
                required:
                  - lead
                additionalProperties: false
        '201':
          description: A new lead was created. `Location` points at it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lead:
                    type: object
                    properties:
                      id:
                        type: integer
                        exclusiveMinimum: 0
                        maximum: 2147483647
                        description: The lead’s ID, new or existing.
                      created:
                        type: boolean
                        description: True when a new lead was created.
                      updated:
                        type: boolean
                        description: >-
                          True when an existing lead was found and at least one
                          empty field on it was filled.
                      archived:
                        description: >-
                          Present when the person matched an archived lead:
                          nothing on it was changed, the enquiry was noted on
                          its timeline, and there is no `Location`.
                        type: boolean
                        const: true
                    required:
                      - id
                      - created
                      - updated
                    additionalProperties: false
                required:
                  - lead
                additionalProperties: false
              example:
                lead:
                  id: 1842
                  created: true
                  updated: false
        '400':
          description: >-
            INVALID_IDEMPOTENCY_KEY: The Idempotency-Key header is longer than
            200 characters or not plain ASCII.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: 'UNAUTHORIZED: The key is missing, wrong, revoked or expired.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            FORBIDDEN: The key lacks the scope, or the organization’s plan does
            not include it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: >-
            INTERNAL: Something failed on our side. Retry; if it persists,
            contact support quoting the `X-Request-Id` response header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >-
            NOT_READY: The organization has no stage for new leads, or no active
            user to own them. Retry after it is set up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - ApiKey:
            - leads.capture
        - ApiKey:
            - leads.write
      x-codeSamples:
        - lang: typescript
          label: Next.js
          source: >-
            // 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 });
            }
        - lang: php
          label: PHP / WordPress
          source: >-
            <?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)));
            }
        - lang: bash
          label: cURL
          source: |-
            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"
              }
            }'
components:
  schemas:
    CaptureAttribution:
      type: object
      properties:
        pageUrl:
          description: The full address of the page the form was on, with its query string.
          type: string
          maxLength: 2000
        pageTitle:
          type: string
          maxLength: 300
        referrer:
          description: The page the visitor came from (document.referrer).
          type: string
          maxLength: 2000
        utmSource:
          type: string
          maxLength: 200
        utmMedium:
          type: string
          maxLength: 200
        utmCampaign:
          type: string
          maxLength: 200
        utmTerm:
          type: string
          maxLength: 200
        utmContent:
          type: string
          maxLength: 200
        formName:
          description: Which form or button on the site, e.g. "Footer enquiry".
          type: string
          maxLength: 200
      additionalProperties: false
      id: CaptureAttribution
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: >-
                Stable, machine-readable: UNAUTHORIZED, FORBIDDEN, NOT_FOUND,
                VALIDATION_FAILED, DUPLICATE_LEAD, RATE_LIMITED…
            message:
              type: string
              description: A sentence for people.
            details:
              description: >-
                For VALIDATION_FAILED, `fields` maps each field key to its
                problem.
              type: object
              propertyNames:
                type: string
              additionalProperties: {}
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      id: Error
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: >-
        An organization API key, created in Settings → Connections → API keys.
        Keys start with `mk_live_`. Marro stores only a hash of a key, so it is
        shown once, when it is created: copy it then. Revoking a key ends its
        access at once. To replace a key without downtime, create the new one,
        switch your server to it, then revoke the old one. Changes made with a
        key appear on the lead’s timeline as made by the person who created the
        key, via the API. Scopes: `leads.read` (Read leads), `leads.write`
        (Create and update leads), `leads.capture` (Capture leads).
      x-scopes:
        leads.read:
          label: Read leads
          description: >-
            Leads with every field, and the lists that describe them: fields,
            stages, sources and people.
          module: crm
        leads.write:
          label: Create and update leads
          description: >-
            Create leads, change their fields, stage and owner, and add notes.
            Needs leads.read.
          module: crm
        leads.capture:
          label: Capture leads
          description: >-
            Capture leads from a website or form: add a lead or update the
            existing one with the same phone or email. Cannot read, list or
            change anything else.
          module: crm
      x-key-types:
        - name: Website form
          scopes:
            - leads.capture
          lifetimeDays: 730
          description: >-
            A website or landing-page form that sends enquiries in as leads. Can
            do nothing else.
        - name: Another application
          lifetimeDays: 90
          description: >-
            Any other integration. Choose the scopes it needs when you create
            it.

````