---
title: "Receive a lead (inbound source, secret in the URL)"
url: https://docs.light-ai.app/api-reference/inbound/receive-inbound-lead-url-secret.md
canonical_url: https://docs.light-ai.app/api-reference/inbound/receive-inbound-lead-url-secret
last_updated: 2026-10-13
summary: "POST /v1/inbound/{source_id}/{token}: For senders that cannot set headers."
api_status: beta
related:
  - https://docs.light-ai.app/api-reference/inbound
---

# Receive a lead (inbound source, secret in the URL)

> POST /v1/inbound/{source_id}/{token}: For senders that cannot set headers.

`POST /v1/inbound/{source_id}/{token}`

For senders that cannot set headers. The URL itself is the credential: keep it private and rotate it if it leaks.

|   |   |
| --- | --- |
| Credential | The source's secret URL token (`lai_whu_…`) in the path. |
| Test mode | Works with test keys (test objects only). |
| Idempotency | `Idempotency-Key` supported. See [Idempotency](https://docs.light-ai.app/idempotency). |

## Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source_id` | string | yes | The inbound source id (`src_…`). Id prefix `src_`. |
| `token` | string | yes | The source's URL token (`lai_whu_…`). |

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string |  | Retry-safe writes: the same key and body replay the first response for 24 hours (`Idempotent-Replayed: true`). 8–128 characters. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source` | string | yes | Where the request came from. One of: `api`, `google_ads_lead_form`, `google_lsa`, `meta_lead_ad`, `highlevel`, `hubspot`, `jobber`, `service_autopilot`, `realgreen`, `sweepandgo`, `deeplawn`, `zapier`, `n8n`, `make`, `crm_import`, `other`. |
| `source_detail` | string \| null |  | At most 200 characters. |
| `external_ref` | object \| null |  | Your id for this request. Retrying with the same ref returns the original lead. |
| `external_ref.system` | string | yes | Lowercase system name, e.g. `jobber`, `hubspot`, `highlevel`. |
| `external_ref.namespace` | string \| null |  | Optional sub-namespace in that system (e.g. an account id). null or "": none. At most 120 characters. |
| `external_ref.id` | string | yes | 1–255 characters. |
| `contact` | object | yes | A name, and an email or a phone. null or "" in an optional field is the same as leaving it out. |
| `contact.first_name` | string \| null |  | At most 100 characters. |
| `contact.last_name` | string \| null |  | At most 100 characters. |
| `contact.email` | string \| null |  | Normalized when valid. An address that is not a valid email is kept as contact.email_raw when the phone works. At most 254 characters. |
| `contact.phone` | string \| null |  | Any common format; normalized to E.164. A number that cannot be normalized is kept as phone_raw when the email works. At most 40 characters. |
| `contact.phone_verified` | boolean \| null |  |  |
| `address` | object \| null |  | Optional. A lead without a street address is stored (a partial address as a hint) and becomes a project when one is added. |
| `address.line1` | string \| null |  | At most 200 characters. |
| `address.line2` | string \| null |  | At most 200 characters. |
| `address.city` | string \| null |  | At most 120 characters. |
| `address.region` | string \| null |  | State or region, e.g. `TX`. At most 120 characters. |
| `address.postal_code` | string \| null |  | At most 20 characters. |
| `address.country` | string \| null |  | ISO 3166-1 alpha-2 in any case, or a common name ("USA", "United States"); normalized to alpha-2. Defaults to US. At most 60 characters. |
| `address.formatted` | string \| null |  | A one-line address, when components are not available. At most 500 characters. |
| `address.lat` | number \| null |  | -90–90. |
| `address.lng` | number \| null |  | -180–180. |
| `address.place_id` | string \| null |  | Google place id, when known (best dedupe key). At most 300 characters. |
| `service_interest` | string \| null |  | One of christmas_lights, permanent_lighting, landscape_lighting, custom; other text is kept and filed as custom. At most 200 characters. |
| `message` | string \| null |  | At most 5000 characters. |
| `attribution` | object \| null |  | utm_*, gclid, campaign ids and similar. |
| `consent` | object \| null |  |  |
| `consent.email` | boolean \| null |  |  |
| `consent.sms` | boolean \| null |  |  |
| `consent.marketing` | boolean \| null |  |  |
| `consent.text` | string \| null |  | The exact consent wording the person agreed to. At most 2000 characters. |
| `consent.captured_at` | timestamp \| null |  | RFC 3339 date-time with offset. |
| `consent.method` | string \| null |  | At most 100 characters. |
| `custom_fields` | object \| null |  |  |
| `submitted_at` | timestamp \| null |  | RFC 3339 date-time with offset. |
| `is_test` | boolean \| null |  | Live keys reject true (use a test key for test data). |

## Response 202

Received (identical for new, repeat and replayed leads).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string | yes | Always `receipt`. |
| `id` | string | yes | Id prefix `rcpt_`. |
| `status` | string | yes | Always `received`. |
| `livemode` | boolean | yes | false for objects created with a test key. |

## Errors

| Status | Meaning |
| --- | --- |
| `400` | Bad request. [`bad_request`](https://docs.light-ai.app/errors#bad_request), [`test_mode_mismatch`](https://docs.light-ai.app/errors#test_mode_mismatch), [`test_mode_unsupported`](https://docs.light-ai.app/errors#test_mode_unsupported), [`unsupported_api_version`](https://docs.light-ai.app/errors#unsupported_api_version) |
| `401` | Missing or invalid credential. [`authentication_required`](https://docs.light-ai.app/errors#authentication_required), [`invalid_api_key`](https://docs.light-ai.app/errors#invalid_api_key), [`api_key_revoked`](https://docs.light-ai.app/errors#api_key_revoked), [`api_key_expired`](https://docs.light-ai.app/errors#api_key_expired), [`api_key_suspended`](https://docs.light-ai.app/errors#api_key_suspended) |
| `403` | Not allowed. [`insufficient_scope`](https://docs.light-ai.app/errors#insufficient_scope), [`plan_upgrade_required`](https://docs.light-ai.app/errors#plan_upgrade_required), [`organization_paused`](https://docs.light-ai.app/errors#organization_paused), [`organization_off_season`](https://docs.light-ai.app/errors#organization_off_season), [`api_disabled`](https://docs.light-ai.app/errors#api_disabled), [`api_access_not_enabled`](https://docs.light-ai.app/errors#api_access_not_enabled), [`ip_not_allowed`](https://docs.light-ai.app/errors#ip_not_allowed), [`forbidden`](https://docs.light-ai.app/errors#forbidden) |
| `404` | Not found (also for objects of other organizations and for organizations without an active grant). |
| `409` | Conflict. [`stale_state`](https://docs.light-ai.app/errors#stale_state), [`conflict`](https://docs.light-ai.app/errors#conflict), [`idempotency_in_progress`](https://docs.light-ai.app/errors#idempotency_in_progress), [`idempotency_scope_mismatch`](https://docs.light-ai.app/errors#idempotency_scope_mismatch), [`quote_already_decided`](https://docs.light-ai.app/errors#quote_already_decided), [`quote_not_current`](https://docs.light-ai.app/errors#quote_not_current), [`billing_review_required`](https://docs.light-ai.app/errors#billing_review_required) |
| `413` | Request bodies are limited to 256 KB. |
| `415` | The body is not JSON: send Content-Type: application/json. [`unsupported_media_type`](https://docs.light-ai.app/errors#unsupported_media_type) |
| `422` | Validation failed. [`idempotency_key_reused`](https://docs.light-ai.app/errors#idempotency_key_reused) |
| `429` | Rate limited or daily cap reached. See Retry-After and the RateLimit headers. [`rate_limited`](https://docs.light-ai.app/errors#rate_limited), [`daily_cap_reached`](https://docs.light-ai.app/errors#daily_cap_reached) |
| `500` | Unexpected server error. Safe to retry with exponential backoff; quote the Request-Id to support. [`internal_error`](https://docs.light-ai.app/errors#internal_error) |
| `503` | Temporarily unavailable. Retry with exponential backoff. [`service_unavailable`](https://docs.light-ai.app/errors#service_unavailable) |

## Examples

**Request**

curl:

```bash
curl -X POST "https://api.light-ai.app/v1/inbound/{source_id}/{token}" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "contact": {
    "first_name": "Dana",
    "last_name": "Reyes",
    "email": "dana@example.com",
    "phone": "+1 (512) 555-0134"
  },
  "address": {
    "line1": "4102 Oak Hollow Dr",
    "city": "Austin",
    "region": "TX",
    "postal_code": "78731"
  },
  "service_interest": "christmas_lights",
  "message": "Roofline, warm white."
}'
```

JavaScript:

```javascript
const response = await fetch('https://api.light-ai.app/v1/inbound/{source_id}/{token}', {
  method: 'POST',
  headers: {
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact": {
      "first_name": "Dana",
      "last_name": "Reyes",
      "email": "dana@example.com",
      "phone": "+1 (512) 555-0134"
    },
    "address": {
      "line1": "4102 Oak Hollow Dr",
      "city": "Austin",
      "region": "TX",
      "postal_code": "78731"
    },
    "service_interest": "christmas_lights",
    "message": "Roofline, warm white."
  }),
});
const data = await response.json();
```

Python:

```python
import os, uuid, requests

response = requests.post(
    "https://api.light-ai.app/v1/inbound/{source_id}/{token}",
    headers={
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
      "contact": {
        "first_name": "Dana",
        "last_name": "Reyes",
        "email": "dana@example.com",
        "phone": "+1 (512) 555-0134"
      },
      "address": {
        "line1": "4102 Oak Hollow Dr",
        "city": "Austin",
        "region": "TX",
        "postal_code": "78731"
      },
      "service_interest": "christmas_lights",
      "message": "Roofline, warm white."
    },
    timeout=30,
)
data = response.json()
```

**Response 202**

```json
{
  "object": "receipt",
  "id": "rcpt_3f9c2a7be41d…",
  "status": "received",
  "livemode": false
}
```
