# Submit a lead

> POST /api/v1/engine/leads: submit a search result, or post a lead straight to an offer.

```http
POST /api/v1/engine/leads
```

Delivers a lead to one offer. ICON runs its full checks again (duplicate, caps, eligibility, required fields, consent) before the lead reaches the buyer, and answers with the outcome.

Rate limit: 120 per minute per key (bursts of up to 120). Every call to this endpoint counts.

## Submitting a search result

Send the `icon_result_id` the consumer chose, the search it came from (`search_lead_id`), the program, the answers to the result's `form_fields` and the consumer's consent. **You do not send the lead again**: ICON builds the submit on the lead of the search the result came from.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `icon_result_id` | string | yes | The result to submit (from search/results). With it, icon_offer_id is taken from the result. |
| `search_lead_id` | string (uuid) | yes | The icon_lead_id of the search the result came from. ICON builds the submit on that search's lead: the lead need not be sent again. |
| `program_id` | string | — | The chosen program (a programs[].value of the result). |
| `icon_is_test_lead` | boolean | — | Test mode: held before any buyer receives it (TEST_LEAD_HELD). |

Plus `icon_campaign_id`, `icon_affiliate_id`, the consent fields ([Consent and TCPA](https://developers.iconroute.io/consent/)) and one key per answered question. Do not send `icon_lead_id` on a result submit: `search_lead_id` links it to the search.

```bash
curl -X POST 'https://iconroute.io/api/v1/engine/leads' \
  -H "Authorization: Bearer $ICON_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "icon_result_id": "api-0b7e4a2c-981234",
  "search_lead_id": "1e8d30bb-7c5f-41b9-8cf1-5e6b57abe999",
  "icon_campaign_id": "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "icon_affiliate_id": "3c9d8e7f-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
  "program_id": "4411",
  "api_field_military": "none",
  "tcpa_consent": true,
  "consent_button_label": "Request information"
}'
```

How ICON builds the submit:

- The lead, tracking and consumer IP come from your search. Any lead field you send with the submit replaces the search's value (for example a corrected phone number, or an answer the search did not have).
- **Consent is not taken from the search**: send `tcpa_consent` (and `consent_button_label`) with every submit, for the statement shown with that result.
- The result must belong to your API key: another affiliate's result answers `404` (`error: "search_result_not_found"`), even if you send a full lead. A campaign you name must be one of the result's (`403`, `error: "result_campaign_mismatch"`).
- Sending the full lead still works.

Every submit is recorded as its own lead, linked to the search.

## Direct posting

When your campaign has an offer you post to directly, skip the search: send the whole lead with your campaign id, the offer id and the consumer's IP in `tracking.ip_address` (required).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `icon_campaign_id` | string | required | ICON campaign ID (UUID) of the campaign this request is for. Required on every search and every submit: ICON never picks a campaign for you. |
| `icon_affiliate_id` | string | required | Your ICON affiliate ID (UUID). Required on every search and every submit, and must be the affiliate your API key belongs to. ICON records the affiliate from the key; this value is only checked against it. |
| `icon_offer_id` | string | required for direct posting | The ICON offer id (UUID). Required when you post a lead straight to an offer; taken from the result when you submit an icon_result_id. |
| `icon_lead_id` | string | optional | The icon_lead_id ICON returned for a lead you posted directly: send it when you retry that post, so ICON recognises the lead. A request that would change a submitted lead is refused (403). Not used on a result submit (send search_lead_id). |

```bash
curl -X POST 'https://iconroute.io/api/v1/engine/leads' \
  -H "Authorization: Bearer $ICON_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "icon_campaign_id": "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "icon_affiliate_id": "3c9d8e7f-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
  "icon_offer_id": "0b7e4a2c-9d8f-4e1a-b2c3-d4e5f6a7b8c9",
  "lead": {
    "personal": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "ada.lovelace@mail.test",
      "phone": "6025552368",
      "date_of_birth": "1990-04-07"
    },
    "address": {
      "address_line_1": "123 N Main St",
      "city": "Phoenix",
      "state": "AZ",
      "zip_code": "85004"
    },
    "education": {
      "education_level": "bachelors",
      "high_school_graduation_year": "2008",
      "start_timeline": "1_3_months",
      "learning_preference": "online"
    },
    "background": {
      "military_affiliation": "none",
      "us_citizen": "yes"
    }
  },
  "tracking": {
    "ip_address": "203.0.113.9",
    "subid": "pub-42",
    "subid2": "creative-7",
    "utm_source": "partner",
    "utm_medium": "email",
    "utm_campaign": "fall-enrollment"
  },
  "program_id": "4411",
  "tcpa_consent": true,
  "trusted_form_cert_url": "https://cert.trustedform.com/0123456789abcdef0123456789abcdef01234567"
}'
```

Your ICON contact gives you the offer id, its consent statement and the questions it requires. A partner offer (one ICON calls through its own API) can only be submitted from a search result.

## Response

Accepted:

```json
{
  "code": "ACCEPTED",
  "status": "accepted",
  "reason": "Lead accepted",
  "success": true,
  "accepted": true,
  "retryable": false,
  "icon_lead_id": "5a2c7e91-3b4d-4f6a-9c8e-2d1f0b7a6e53",
  "payout": 28,
  "message": "Lead processed successfully"
}
```

Refused (here, ICON already has this lead for the offer):

```json
{
  "code": "DUPLICATE",
  "status": "rejected",
  "reason": "Duplicate - Offer",
  "source": "icon",
  "success": false,
  "accepted": false,
  "retryable": false,
  "icon_lead_id": "5a2c7e91-3b4d-4f6a-9c8e-2d1f0b7a6e53",
  "payout": 0,
  "message": "Duplicate lead"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | — | ACCEPTED, or the reason it was not (DUPLICATE, NOT_ELIGIBLE, CAP_REACHED, BUYER_REJECTED, …). |
| `status` | string | — | The outcome, from one lower-case set. One of: `accepted`, `saved`, `pending`, `held`, `rejected`, `invalid`, `failed`. |
| `reason` | string | — | A short sentence-case explanation, e.g. "Missing HS grad year" or "Duplicate - Client" (" - Client": the school's own system decided). |
| `success` | boolean | — | — |
| `accepted` | boolean | — | — |
| `retryable` | boolean | — | — |
| `icon_lead_id` | string | — | — |
| `payout` | number | — | Accepted: YOUR payout booked for this lead (absent when none is configured). Not accepted: 0. Never the offer's own price. |
| `message` | string | — | — |
| `disposition` | object | — | — |

The answer's `icon_lead_id` is the id of the lead this submit created, not your search id: store it with your own lead.

A decision (accepted or refused by ICON or the buyer) answers HTTP `200`, and so do most refusals for a missing or invalid answer or consent (`status: "invalid"`): read `status` and `code`, not the HTTP status. `PENDING` (HTTP 200 or 202) is not final: the buyer has not decided yet. The reasons are standardized: `Duplicate - Offer` means ICON already has the lead for that offer, `Duplicate - Client` means the school's own system said duplicate. See [Standard responses](https://developers.iconroute.io/responses/).

## Errors

| HTTP | Meaning |
| --- | --- |
| 400 | REQUIRED_FIELD_MISSING / INVALID_FIELD (the field is named). |
| 401 | AUTH_REQUIRED (no API key) or AUTH_INVALID (a key ICON does not know); status invalid. |
| 403 | FORBIDDEN: the campaign or offer is not yours, icon_affiliate_id is not your key's affiliate, the campaign you named is not one of the result's (error "result_campaign_mismatch"), or the lead was already submitted. |
| 404 | NOT_FOUND: the result, lead or offer is unknown to your key (another affiliate's result answers error "search_result_not_found"). |
| 409 | MALFORMED_REQUEST: send search_lead_id (the icon_lead_id of the search the result came from). |
| 429 | RATE_LIMITED (retryable): this API key is over its rate limit. Wait the Retry-After header's seconds (also retry_after_seconds) and retry. Limits are per API key and per leg: search 60 per minute with bursts of up to 10, results polling 600 per minute, submit 120 per minute, unless ICON set other limits for your key. |
| 500 | INTERNAL_ERROR (request_id identifies it to ICON support). |
| 503 | UNAVAILABLE: retry later. |
