URL: /code/apps/forms-and-waitlists/identify · Id: `page:/code/apps/forms-and-waitlists/identify`

# Visitor.identify

> Joins an anonymous Visitor to the Contact they turn out to be, so their earlier visits count.

POST https://api.sb/&#123;your-domain&#125;/visitors/identify

Joins an anonymous [Visitor](https://startupbuilder.dev/business/nouns/visitor) to the [Contact](https://startupbuilder.dev/business/nouns/contact) they turn out to be, so their earlier visits count.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Visitor.identified`
- **Price:** Free

```text title="Run it · Ask"
Maria just filled the demo form: join her earlier visits to her contact.
```

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

const result = await $.try.Visitor.identify({ visitor: 'visitor_Xs4dF8gH2j', contact: 'contact_FdmRdAKcbV' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "contact": {
    "$id": "https://api.sb/leaseline.app/contacts/contact_FdmRdAKcbV",
    "$type": "Contact",
    "name": "Maria Chen",
    "email": "maria@harborview-pm.com",
    "visits": 3
  },
  "visitor": {
    "$id": "https://api.sb/leaseline.app/visitors/visitor_Xs4dF8gH2j",
    "$type": "Visitor",
    "contact": "https://api.sb/leaseline.app/contacts/contact_FdmRdAKcbV",
    "pages": [
      "/pricing",
      "/pricing",
      "/rent-ledger-template"
    ]
  },
  "action": {
    "operation": "Visitor.identify",
    "mode": "try",
    "status": "done"
  }
}
```

Its Inputs, Outputs, Effects and guards quote [the hub’s `Visitor.identify`](https://startupbuilder.dev/business/nouns/visitor/identify); this page adds what each Venue honors, its limits and a worked call.

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `visitor` | `Visitor` | Required | The Visitor. |
| `contact` | `Contact` | Required | The Contact or customer the Visitor is. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `contact` | `Contact` | Never null: the Contact, with every earlier Visit of the Visitor attached. |
| `visitor` | `Visitor` | Never null: the Visitor, joined to its Contact from now on. |

## Effects
**Action class**

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

**Modes**

- `try`: Returns the Contact with the Visits it would gain; nothing is joined.
- `send`: Returns a pending Action at once; `Visitor.identified` fires when the Visits are moved.
- `do`: Joins them and returns both records.

**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 Visitor already joined to this Contact returns both as they stand; joining it to a second Contact refuses.

**Saved or shared**

The join, and each earlier Visit now counted for the Contact. It runs on its own at sign-up, sign-in and checkout; a call covers a form or an import.

**Emits**

`Visitor.identified` with `{ visitor, contact, visits }`

**Performed by**

[Marketing lead](https://startupbuilder.dev/agents/marketing/marketing-lead)

**Price**

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

## On each Venue
It acts on your own records and reaches no Venue, so every account answers the same way.

## What try does
Runs every check the call meets and returns the record as it would be; nothing is stored, sent or posted.

## Refuses when
This call has no refusals of its own.

## When it waits
In `send` mode it returns a pending Action, `reason: 'running'` while the Visits move, and `Visitor.identified` delivers the result.

```ts
await $.Watch.create({ selector: 'Visitor.identified', url: 'https://leaseline.app/hooks/sb' })
```

## Example
In `send` mode the call returns its pending Action at once, and the Event below delivers the result.

**Ask**

```text
Maria just filled the demo form: join her earlier visits to her contact.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/visitors/identify?mode=send" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "visitor": "visitor_Xs4dF8gH2j",
  "contact": "contact_FdmRdAKcbV"
}'
```

**CLI**

```bash
npx startup-builder do Visitor.identify --send --data '{
  "visitor": "visitor_Xs4dF8gH2j",
  "contact": "contact_FdmRdAKcbV"
}'
```

`--send` returns the pending Action at once.

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "send",
    "code": "return await $.Visitor.identify({ visitor: 'visitor_Xs4dF8gH2j', contact: 'contact_FdmRdAKcbV' })"
  }
}
```

**SDK**

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

const result = await $.send.Visitor.identify({
  visitor: 'visitor_Xs4dF8gH2j',
  contact: 'contact_FdmRdAKcbV',
})
```


```json title="Response, in send mode"
{
  "mode": "send",
  "action": {
    "$id": "https://api.sb/leaseline.app/actions/action_Sd3kQ9vT2w",
    "operation": "Visitor.identify",
    "status": "pending",
    "reason": "running"
  }
}
```

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/checks#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `visitor_test_known` | `{"contact":{"visits":3}}` | Always three earlier Visits, so the first touch that names what they read can be tested. |

## Used by
None.

## Related
[`Waitlist.join`](/code/apps/forms-and-waitlists/join) · [`Waitlist.admit`](/code/apps/forms-and-waitlists/admit) · [`Visitor.merge`](/code/apps/forms-and-waitlists/merge) · [`Form.create`](/code/apps/forms-and-waitlists/create) · [`Form.update`](/code/apps/forms-and-waitlists/update)