URL: /code/apps/crm/qualify · Id: `page:/code/apps/crm/qualify`

# Lead.qualify

> Confirm a lead fits your offer and open its deal, with its value.

`POST https://api.sb/{your-domain}/leads/qualify`

Confirm a [lead](https://startupbuilder.dev/business/nouns/lead) fits your [offer](https://startupbuilder.dev/business/nouns/offer) and open its [deal](https://startupbuilder.dev/business/nouns/deal), with its value.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Lead.qualified`
- **Sites:** [4 sites](#on-each-venue)
- **Price:** Free

```text title="Run it · Ask"
Rosa booked Tuesday’s call: qualify Northgate Rentals for Leaseline and open the Deal.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Lead.qualify({ lead: 'lead_Nr5gT1aV7k', offer: 'offer_Ls1mP4qR8v' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "deal": {
    "$type": "Deal",
    "status": "Open",
    "value": {
      "amount": 1788,
      "currency": "USD"
    },
    "nextStep": "Discovery call, Tuesday, November 3, 2:00 p.m. Central"
  },
  "lead": {
    "$id": "https://api.sb/leaseline.app/leads/lead_Nr5gT1aV7k",
    "$type": "Lead",
    "status": "working",
    "fit": "strong",
    "interest": "Can you send times for Tuesday?"
  },
  "action": {
    "operation": "Lead.qualify",
    "mode": "try",
    "status": "done"
  }
}
```

Opens a deal for a lead that fits one of your offers, with its value and the reason it fits. [The hub’s definition of `Lead.qualify`](https://startupbuilder.dev/business/nouns/lead/qualify).</HubDefinition>

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `lead` | `Lead` | Required | The lead to qualify. |
| `offer` | `Offer` | Required | The offer whose ICP the lead matches. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `deal` | `Deal` | Never null. |
| `lead` | `Lead` | Never null. |
| `deal.status` | `string` | Never null: `Open` when the deal starts. |
| `deal.value` | `money`, in USD | Never null: the offer’s first-year price, from its price and interval. |
| `deal.nextStep` | `text` | Null when no dated step is agreed yet; a booked Meeting sets it. |
| `lead.status` | `string` | Never null: `working` once qualified. |
| `lead.fit` | `string` | Never null once graded: `strong` or `partial`. A lead graded `none` is not qualified. |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Grades the lead’s fit against the offer’s ICP and returns the deal it would open; nothing is stored.
- `send`: Opens the deal at once and returns it.
- `do`: Opens the deal at once and returns it.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. A lead with an open deal for the same offer returns that deal; it never opens a second.

**Saved or shared**

The deal is saved with the lead and its Contact, and mirrored to your CRM when one is connected.

**Emits**

`Lead.qualified` with `{ lead, deal, offer, fit }`

**Performed by**

[Sales Engineer](https://startupbuilder.dev/agents/sales/sales-engineer)

**Price**

Free on your own account: the platform charges nothing for the call.

## On each Venue
| Site | Fields | Formats |
| --- | --- | --- |
| [Facebook](#facebook) | `interest` | 1 |
| [LinkedIn](#linkedin-ads) | `interest` | 3 |
| [Facebook and Instagram](#meta-ads) | `interest` | 5 |
| [The CRM you already use](#your-crm) | `status`, `fit`, `owner` | None. |

### Facebook
Through [Facebook](/code/integrations/facebook).

| Our field | Facebook’s field |
| --- | --- |
| `Lead.interest` | `leadgen.field_data` (Each form question and answer, as you wrote the form) |

- **Lead-form entries.** Arrive by webhook within seconds; the agent qualifies each and drafts the first reply within 5 minutes.
- **Your role on the page.** Posting needs a role that can post; a role removed in Meta’s settings ends the connection with `needs-reauth`.

Feed image ad, 1440 × 1800, 4:5

### LinkedIn
Through [LinkedIn Ads](/code/integrations/linkedin-ads).

| Our field | LinkedIn Ads’s field |
| --- | --- |
| `Lead.interest` | `leadFormResponse answers` |

Feed post Single image ad, 1200 × 628, 1200 × 1200 or 720 × 900 Document ad

### Facebook and Instagram
Through [Meta Ads](/code/integrations/meta-ads).

| Our field | Meta Ads’s field |
| --- | --- |
| `Lead.interest` | `leadgen.field_data` |

Facebook: Feed image ad, 1440 × 1800, 4:5 Instagram: Feed image, 320 to 1,440 pixels wide, 4:5 to 1.91:1 Instagram: Carousel Instagram: Reel, 9:16 Instagram: Story, 9:16

### The CRM you already use
Through [The CRM you already use](/code/integrations/your-crm).

| Our field | The CRM you already use’s field |
| --- | --- |
| `Lead.status` | `lead status or lifecycle stage` (Whichever your CRM keeps) |
| `Lead.fit` | `fit` (A field the platform adds: `strong`, `partial` or `none`) |
| `Lead.owner` | `owner` |

- **Direction.** One per Integration. Your records stay the record in both modes, and the side you declared leads every conflict.
- **Stages.** Map one to one onto the pipeline you picked; a stage with no match refuses with `venue-rejected` and names it.

## What try does
| Account | What try does |
| --- | --- |
| [Facebook](/code/integrations/facebook#try) | Returns the post as your page will show it, with the time it goes live; nothing is posted or scheduled. |
| [LinkedIn Ads](/code/integrations/linkedin-ads#try) | Reads your ad account, the matched count of each audience and the targeting facets, and checks the campaign against LinkedIn’s budget minimums; LinkedIn has no validate-only call, so nothing is sent to it and nothing is created. |
| [Meta Ads](/code/integrations/meta-ads#try) | Sends the whole build with Meta’s `validate_only` execution option: Meta runs every check and creates nothing, and the response names any field it would reject. |
| [The CRM you already use](/code/integrations/your-crm#try) | Reads your CRM and returns what one sync would change on each side, record by record; nothing is written to either. |

## Refuses when
This call has no refusals of its own. Every call through an account can also meet [the shared account refusals](/code/refusals#accounts): [`grant-missing`](/code/refusals#grant-missing), [`needs-reauth`](/code/refusals#needs-reauth), [`account-not-connected`](/code/refusals#account-not-connected), [`over-mandate`](/code/refusals#over-mandate), [`quota-reached`](/code/refusals#quota-reached), [`venue-rejected`](/code/refusals#venue-rejected).

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
Rosa booked Tuesday’s call: qualify Northgate Rentals for Leaseline and
open the Deal.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/leads/qualify?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"lead":"lead_Nr5gT1aV7k","offer":"offer_Ls1mP4qR8v"}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Lead.qualify --try --data '{"lead":"lead_Nr5gT1aV7k","offer":"offer_Ls1mP4qR8v"}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Lead.qualify({ lead: 'lead_Nr5gT1aV7k', offer: 'offer_Ls1mP4qR8v' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Lead.qualify({
  lead: 'lead_Nr5gT1aV7k',
  offer: 'offer_Ls1mP4qR8v',
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `lead_test_strong` | `{"fit":"strong","deal":{"status":"Open","value":{"amount":1788,"currency":"USD"}}}` | A strong-fit test lead opens the same $1,788 deal every time, so your pipeline steps run against a known value. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

## Used by
- **Workflows:** [Inbound routing](/capabilities/convert-visitors/inbound-routing)
- **Agent tasks:** [Answer every inbound](/capabilities/convert-visitors/answer-every-inbound)
- **Services:** [Booked first calls](/capabilities/reach-out/booked-first-calls)
- **Integrations:** [Facebook](/code/integrations/facebook) · [LinkedIn Ads](/code/integrations/linkedin-ads) · [Meta Ads](/code/integrations/meta-ads) · [The CRM you already use](/code/integrations/your-crm)

## Related
None.