# Recipe: Create a lead with a quote request

Capture a new sales prospect together with the products they want a quote on
in a single API call to the Colab Commerce v1 REST API.

## Endpoint

`POST https://api.colabcommerce.com/v1/leads`

- Content type: `application/json`
- Returns `201 Created` with the serialized lead on success.
- The newly-created quote request is the first element of
  `lead.lead_activities` in the response.

## Auth

- Header: `X-Api-Key: <api_key>`
- The key may optionally be prefixed with an identifier and a colon — the
  effective key is the substring after the last `:`.
- The authenticated user determines the lead's `owner` (see Constraints).
- 401 if missing/invalid; 403 if the authenticated user is not authorized to
  create leads under the supplied `assignee`.

## Required inputs

- `lead.name` — string. The shopper's display name.
- At least one of:
  - `lead.emails[]` — each item: `{ "email": "<email>" }`
  - `lead.phone_numbers[]` — each item:
    `{ "phone_number": "<E.164 or local>", "country_code": "<ISO-2 country>" }`
- `lead.lead_activities[]` — at least one entry. For this recipe, exactly one
  `LeadActivity::QuoteRequest` (see below).
- For the nested quote request:
  - `type` — must be the string `"LeadActivity::QuoteRequest"`.
  - `data.products[]` — at least one product. Each product is a free-form
    hash; recognized keys are `name`, `category`, `quantity`, `price`, `sku`,
    `url`, `image_url`, `options`.

## Optional inputs

- `lead.location` — object: `street_line_one`, `street_line_two`, `city`,
  `province`, `postal_code`, `country`. Geocoded server-side.
- `lead.assignee_type` + `lead.assignee_id` — pre-assign the lead to a
  retailer location (see Constraints).
- Quote request `source_type` + `source_id` — typically the same
  `CompanyRetailerLocation` as the assignee when the lead came from a
  store-specific page.
- Quote request `data.notes` — freeform shopper message string.
- Quote request `data.additionalData` — arbitrary JSON object passed through
  to the stored activity (campaign attribution, referrer, form metadata,
  etc.).

## Constraints

- `lead.owner` is **always** set server-side to the authenticated user's
  `Company`. Any `owner_id` / `owner_type` you send is ignored.
- `lead.assignee` is **optional**. When supplied it **must** be a
  `CompanyRetailerLocation` belonging to the same company:
  - `assignee_type` must be the exact string `"CompanyRetailerLocation"`.
  - `assignee_id` must be the UUID of an existing `CompanyRetailerLocation`
    owned by the authenticated user's company.
  - Omit both fields to leave the lead unassigned for downstream
    round-robin or manual assignment.
- `lead.lead_activities[].type` must be a valid STI subclass string. For this
  recipe use `"LeadActivity::QuoteRequest"`. Unknown subclasses return
  `422` with `details[].code = "invalid"` on field `type`.
- `data.products` recognized keys: `name`, `category`, `quantity`, `price`,
  `sku`, `url`, `image_url`, `options`. Any other keys are preserved on the
  stored activity but ignored by the model.

## Request body — unassigned (most common)

```json
{
  "lead": {
    "name": "Jane Customer",
    "emails": [{ "email": "jane@example.com" }],
    "phone_numbers": [
      { "phone_number": "+15555550100", "country_code": "US" }
    ],
    "location": {
      "street_line_one": "123 Main St",
      "city": "Calgary",
      "province": "AB",
      "postal_code": "T2P 1J9",
      "country": "CA"
    },
    "lead_activities": [
      {
        "type": "LeadActivity::QuoteRequest",
        "data": {
          "notes": "Looking for delivery before the long weekend.",
          "products": [
            {
              "name": "Westwood Sofa",
              "sku": "WS-3001-CHAR",
              "category": "Sofas",
              "quantity": 1,
              "price": 1899.0,
              "url": "https://example.com/products/westwood-sofa",
              "image_url": "https://example.com/img/ws-3001.jpg",
              "options": {
                "color": "Charcoal",
                "fabric": "Performance Weave"
              }
            }
          ]
        }
      }
    ]
  }
}
```

## Request body — pre-assigned to a retailer location

Use this shape when your form already knows which `CompanyRetailerLocation`
should handle the lead (for example, a "request a quote from this store"
button on a store detail page). The assignee **must** be a
`CompanyRetailerLocation` owned by your company.

```json
{
  "lead": {
    "name": "Jane Customer",
    "assignee_type": "CompanyRetailerLocation",
    "assignee_id": "9b8f0d2e-1a3c-4b6d-8e9f-0a1b2c3d4e5f",
    "emails": [{ "email": "jane@example.com" }],
    "phone_numbers": [
      { "phone_number": "+15555550100", "country_code": "US" }
    ],
    "lead_activities": [
      {
        "type": "LeadActivity::QuoteRequest",
        "source_type": "CompanyRetailerLocation",
        "source_id": "9b8f0d2e-1a3c-4b6d-8e9f-0a1b2c3d4e5f",
        "data": {
          "notes": "Customer requested in-store pickup.",
          "products": [
            {
              "name": "Westwood Sofa",
              "sku": "WS-3001-CHAR",
              "quantity": 1,
              "price": 1899.0,
              "options": { "color": "Charcoal" }
            },
            {
              "name": "Westwood Loveseat",
              "sku": "WL-3002-CHAR",
              "quantity": 1,
              "price": 1499.0
            }
          ],
          "additionalData": {
            "campaign": "spring-2026",
            "referrer": "https://example.com/landing/spring-promo"
          }
        }
      }
    ]
  }
}
```

## cURL

```bash
curl -X POST "https://api.colabcommerce.com/v1/leads" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $CC_API_KEY" \
  -d @lead.json
```

## JavaScript (Node 18+)

Never call this from a browser — API keys must stay server-side.

```js
const res = await fetch('https://api.colabcommerce.com/v1/leads', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.CC_API_KEY,
  },
  body: JSON.stringify({
    lead: {
      name: 'Jane Customer',
      emails: [{ email: 'jane@example.com' }],
      phone_numbers: [
        { phone_number: '+15555550100', country_code: 'US' },
      ],
      lead_activities: [
        {
          type: 'LeadActivity::QuoteRequest',
          data: {
            notes: 'Looking for delivery before the long weekend.',
            products: [
              {
                name: 'Westwood Sofa',
                sku: 'WS-3001-CHAR',
                quantity: 1,
                price: 1899.0,
                options: { color: 'Charcoal' },
              },
            ],
          },
        },
      ],
    },
  }),
})

if (!res.ok) {
  const error = await res.json()
  throw new Error(`Lead create failed: ${res.status} ${JSON.stringify(error)}`)
}

const { lead } = await res.json()
```

## Response (201 Created)

```json
{
  "lead": {
    "id": "f1c0e7d4-...-...",
    "name": "Jane Customer",
    "status": "New",
    "created_at": "2026-05-19T15:42:11Z",
    "emails": [{ "id": "...", "email": "jane@example.com" }],
    "phone_numbers": [
      {
        "id": "...",
        "phone_number": "+15555550100",
        "country_code": "US",
        "formatted": "+1 555-555-0100"
      }
    ],
    "location": {
      "id": "...",
      "street_line_one": "123 Main St",
      "city": "Calgary",
      "province": "AB",
      "postal_code": "T2P 1J9",
      "country": "CA",
      "latitude": 51.0447,
      "longitude": -114.0719
    },
    "lead_activities": [
      {
        "id": "...",
        "type": "LeadActivity::QuoteRequest",
        "data": {
          "notes": "Looking for delivery before the long weekend.",
          "products": [
            {
              "name": "Westwood Sofa",
              "sku": "WS-3001-CHAR",
              "quantity": 1,
              "price": 1899.0,
              "...": "..."
            }
          ]
        }
      }
    ]
  }
}
```

## Common errors

All errors follow the standard v1 envelope:
`{ "status": <int>, "error": "<reason>", "message": "<human>", "errors": { ... }, "details": [ { "field", "code", "message", "full_message" } ] }`.

- `401 Unauthorized` — missing or unknown `X-Api-Key`.
- `403 Unauthorized` (status 403) — authenticated, but the supplied
  `assignee` is not a `CompanyRetailerLocation` your user can route leads to.
- `422 Unprocessable Entity` — validation failure. Likely causes:
  - `name` blank → `details[].code = "blank"`.
  - No `emails` and no `phone_numbers` → contact-channel validation error.
  - `phone_numbers[].phone_number` malformed → `code = "invalid"`.
  - `lead_activities[].type` not a known STI subclass →
    `code = "invalid"` on field `type`.
  - `data.products` missing or empty → quote-request validation error.
  - Missing root parameter (`{}` instead of `{ "lead": { ... } }`) →
    `code = "missing"` on field `lead`.
- `400 Bad Request` — malformed JSON body.

## See also

- API reference: `/developers/api/v1`
- Leads endpoints: `/developers/api/v1#leads`
- Lead activities (STI subclasses): `/developers/api/v1#lead-activities`
- Company retailer locations (valid `assignee` targets):
  `/developers/api/v1#company-retailer-locations`
- Errors envelope: `/developers/api/v1#errors`
