# Show who is online

**You will:** Send presence signals from your backend and read online and last seen on contacts and cases.

**You need:**
- A key with contacts:presence, and contacts:presence-read to read it
- Contacts that carry your user id as externalRef
- The organization setting presenceEnabled turned on

Presence is one fact per contact: is this person in your app right now. Your backend reports it. Lelemon does not watch anyone by itself.

## Turn presence on

Presence is off until you turn it on. While it is off, reading presence or filtering with `presence=online` answers `409 presence_not_configured`. Reporting signals needs `contacts:presence`; reading or changing the settings needs `presence:configure`, which a key issued by an admin or the owner can hold.

```bash title="cURL"
curl -X PUT https://api.lelemon.cl/v2/presence/settings \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "enabled": true }'
```

```ts title="Node.js"
await lelemon.presence.settings.update({ enabled: true });
```


`GET /v2/presence/settings` says why presence does not show: it needs `enabled` true and `hasSignals` true, which becomes true after the first signal arrives.

## Create the contact first

A signal does not create contacts. Create them with your own id as `externalRef`:

```bash title="cURL"
curl -X PUT https://api.lelemon.cl/v2/contacts/by-external-ref/crm-4471 \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "name": "Ana Pérez" }'
```

```ts title="Node.js"
await lelemon.contacts.upsertByExternalRef('crm-4471', { name: 'Ana Pérez' });
```


## Send signals

Send `active` about every 60 seconds while the person is in your app, and `offline` when they leave. One call takes up to 100 signals.

```bash title="cURL"
curl https://api.lelemon.cl/v2/presence-signals \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{
    "signals": [
      { "externalRef": "crm-4471", "state": "active" },
      { "externalRef": "crm-4480", "state": "offline" }
    ]
  }'
```

```ts title="Node.js"
await lelemon.presence.reportSignals({
  signals: [
    { externalRef: 'crm-4471', state: 'active' },
    { externalRef: 'crm-4480', state: 'offline' },
  ],
});
```


```json title="Response (202)"
{ "object": "presence_signals_result", "accepted": 1 }
```


**PresenceSignal**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `externalRef` | string | yes | The externalRef you set on the contact. A reference that matches no contact of this organization is ignored. |
| `state` | enum | yes | active: the person is in your app right now, send it about every 60 seconds while they are. offline: they left; they stop showing as online at once. `active`, `offline` |

`accepted` is how many signals matched a contact of your organization. It never says which ones did not. A reference that matches no contact is ignored. Sending the same signal twice is harmless.

## What Lelemon keeps

Only the last signal of each contact. There is no history and no webhook.

A contact is online while its last `active` signal is less than 120 seconds old and no `offline` came after it. Once offline, `lastSeenAt` can be up to 5 minutes behind.

## Read presence

Presence appears in contacts and cases when `presenceEnabled` is on, at least one signal has arrived and the key has `contacts:presence-read`.

```bash title="cURL"
curl "https://api.lelemon.cl/v2/cases?presence=online" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
for await (const kase of lelemon.cases.list({ query: { presence: 'online' } })) {
  console.log(kase.id, kase.contact?.presence);
}
```


```json title="presence on a contact"
{ "online": true, "lastSeenAt": "2026-11-02T15:04:11.000Z" }
```


**Presence**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `online` | boolean | yes | true when the last active signal is less than 120 seconds old and no offline signal came after. |
| `lastSeenAt` | string or null | yes | The last time a signal arrived. While online it is exact; once offline it can be up to 5 minutes behind. null when no signal ever arrived for this contact. |

`presence=online` also works on `GET /v2/contacts`. Without the scope, the `presence` field is left out. If presence is not active, the filter answers `409 presence_not_configured`.
