---
title: "Public API — Alphie"
description: "Reference for the Alphie Public API: a read-only REST API for syncing the companies and contacts Alphie surfaces on your website into your own CRM or data warehouse."
last_updated: "2026-07-24"
canonical: "https://meetalphie.com/api"
---

Developers

# Public API

A read-only REST API for syncing the companies and contacts Alphie surfaces on your website into your own CRM or data warehouse. All requests and responses use JSON with `camelCase` field names.

v1 Read-only REST JSON

On this page On this page

- Overview
- Base URL
- Authentication
- Rate Limits
- Pagination
- Incremental Sync
- GET /companies
- GET /contacts
- GET /visitors
- GET /visitors/{id}
- GET /sessions
- Privacy and data handling
- Errors
- Versioning

## Overview

Alphie identifies companies that visit your website and captures or discovers contacts at those companies. The Public API gives you programmatic, read-only access to that data so you can keep your own systems — most commonly a CRM — continuously up to date, without any manual export or import step.

Four resources are available:

- **Companies** — organizations Alphie has identified visiting your site, along with firmographic data and a fit score against your ideal customer profile.
- **Contacts** — people captured by your Alphie chat widget and forms, plus people Alphie has discovered at companies it identified.
- **Visitors** — people who browsed your site with the Alphie widget present, whether or not they ever identified themselves. One row per person, with where they first came from and what they did across every visit.
- **Sessions** — individual visits. A visitor accumulates a new session each time they come back, so this is the resource to aggregate over for traffic, referrer and campaign reporting.

**Visitors and contacts overlap on purpose.** Someone who chats with your widget is both: they browsed your site *and* they left their details. Those rows appear in **both** lists, with the **same id** and with `isContact: true` on the visitor row — so you can join the two without guessing. GET /contacts is the list of people you can follow up with; GET /visitors is everyone who showed up, which is what a funnel needs as its denominator.

The API is designed to be **polled on a schedule**: do an initial backfill, then periodically ask for what's new or changed since your last successful sync. See Incremental Sync for a recommended approach.

This version of the API is read-only — it does not support creating, updating, or deleting data in Alphie.

Installing the Alphie widget rather than reading data? See the [install guide for coding agents](https://meetalphie.com/llms-install.md), which covers the script tag, where it belongs in your app, and how to verify the install.

## Base URL

```
https://q.meetalphie.com/api/public/v1
```

If your account is served from a dedicated Alphie host, use that host in place of `q.meetalphie.com` in every example in this document — the path (`/api/public/v1/...`) stays the same.

## Authentication

Requests are authenticated with an **API key**, issued to you by your Alphie account administrator. API keys look like `alph_` followed by 48 hexadecimal characters, for example:

```
alph_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
```

The full key value is shown to your admin **exactly once**, at creation time. Alphie retains only a one-way hash of it and cannot display it again afterward, so store it securely (e.g. in a secrets manager) as soon as it's created.

Send the key on every request, using either of the following headers:

| Header | Example |
| `Authorization` (primary) | `Authorization: Bearer alph_a1b2c3d4e5f6…` |
| `X-API-Key` (alternative) | `X-API-Key: alph_a1b2c3d4e5f6…` |

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/companies"
```

A few things to keep in mind:

- API keys are **read-only** in this version and are scoped to your Alphie account — a key only ever returns data belonging to the account it was created under.
- An admin can revoke a key at any time. Requests made with a revoked key receive `401 API key revoked` (see Errors). Revocation is immediate and cannot be undone — issue a new key if you need to reconnect.

## Rate Limits

The API allows **120 requests per minute**. Every response includes headers describing your current standing against that limit:

| Header | Description |
| `X-RateLimit-Limit` | The maximum number of requests allowed in the current window. |
| `X-RateLimit-Remaining` | The number of requests you have left in the current window. |
| `X-RateLimit-Reset` | When the current window resets. |

If you exceed the limit, the API responds with `429 Too Many Requests`:

json

```json
{
  "error": "Too many requests",
  "retryAfter": 30
}
```

`retryAfter` is the number of seconds to wait before retrying. Back off for at least that long, and consider spacing out your requests (for example, pausing briefly between pages) rather than retrying immediately in a loop.

The limit is **shared across every endpoint**, not per endpoint. A large `GET /sessions` backfill will therefore eat into the budget a CRM sync is using for `GET /contacts`. If you run both, stagger them rather than starting them together.

## Pagination

The page-numbered list endpoints — `GET /companies`, `GET /contacts` and `GET /visitors` — return results in a common envelope:

json

```json
{
  "data": [ ... ],
  "page": 1,
  "pageSize": 25,
  "total": 214
}
```

| Field | Type | Description |
| `data` | array | The results for this page. |
| `page` | number | The page number returned. |
| `pageSize` | number | The number of records requested per page. |
| `total` | number | The total number of records matching the query, across all pages. |

All three accept the following pagination parameters:

| Parameter | Type | Default | Description |
| `page` | integer | `1` | Page number to retrieve. |
| `pageSize` | integer | `25` | Number of records to return per page. Maximum `100`. |

Ordering is a **total order within a single snapshot**: rows are sorted by a timestamp with the record `id` as a tie-breaker, so two rows never swap places at random. It is **not** stable across a multi-page walk, and these endpoints fail in two different ways if data changes while you are paging:

| Endpoint | Sorted by | If data changes mid-walk |
| `GET /companies`, `GET /visitors` | most recent activity, which **moves** | A company or visitor that becomes active jumps toward page 1 and can push a record you have not read yet across a boundary you already crossed — that record is **skipped**. |
| `GET /contacts` | creation time, which never changes | New records are inserted at the head and shift the offsets, so you may see a record **twice**. Nothing is skipped. |

Neither is a problem in practice if you follow the incremental sync recipe: upsert by `id` (which makes duplicates harmless), and re-run the walk on your next poll with the appropriate cursor (which picks up anything skipped). If you need a long backfill to be exactly complete in one pass, run it during a quiet period, or page with `lastSeenSince`/`createdSince` windows narrow enough that records cannot move between them.

`GET /sessions` is not affected by any of this — see below.

### Cursor pagination (`GET /sessions`)

`GET /sessions` uses a different envelope, because a visit's start time never changes once recorded:

json

```json
{
  "data": [ ... ],
  "pageSize": 25,
  "nextCursor": "eyJzIjoxNzg0MDAwMDAwMDAwLCJpIjoiYnMtOSJ9"
}
```

Pass `nextCursor` back as the `cursor` parameter to get the next page; a `nextCursor` of `null` means you have reached the end. Because the sort key is immutable, this walk cannot skip or repeat a record no matter what happens while you are paging.

The cursor is **opaque** — treat it as a string, pass it back byte for byte, and do not parse or construct one yourself. Its internal format may change.

There is deliberately **no total** on this endpoint. Counting every session an account has ever recorded costs time proportional to your whole history, on every request, and we would rather not charge you that on each page just to print a number. Page until `nextCursor` is `null`.

**Note:** `page` and `pageSize` values outside their valid range (for example, `pageSize=500` or `page=0`) are rejected with `400 Invalid query parameters` — they are not silently clamped to the nearest valid value. Validate these on your side before sending requests.

## Incremental Sync

Rather than re-fetching everything on every sync, use each endpoint's cursor to ask Alphie only for what's changed:

| Endpoint | Cursor | Meaning |
| `GET /companies` | `lastSeenSince` | Companies whose most recent visit is at or after this instant. |
| `GET /contacts` | `createdSince` | Contacts captured or discovered at or after this instant. |
| `GET /visitors` | `lastSeenSince` | Visitors active at or after this instant. (`createdSince` is also available, for visitors *first seen* since a given time.) |
| `GET /sessions` | `startedSince` | Visits that began at or after this instant. |

A typical integration looks like this:

- **Initial backfill.** Starting at `page=1` with a large `pageSize` (up to `100`), page through the full result set — increment `page` until the number of records returned is less than `pageSize`, or you've retrieved `total` records. Upsert every record you receive into your CRM, keyed by its `id`.
- **Record a checkpoint.** Once the backfill finishes successfully, store the current time as your `lastSyncedAt` checkpoint.
- **Poll incrementally.** On each subsequent run, call the endpoint with `lastSeenSince` (companies) or `createdSince` (contacts) set to your stored checkpoint, **minus about one minute**. That small overlap protects against clock skew and against records that were still being written at the exact instant of your last checkpoint. Page through the results the same way as the backfill.
- **Upsert by id.** Because the overlap window may return a few records you've already synced, always upsert (insert-or-update) by `id` rather than inserting blindly. This makes each sync run idempotent and safe to retry or re-run.
- **Advance the checkpoint.** After a poll completes successfully, move your stored `lastSyncedAt` forward to the time you *started* that poll (again, before the one-minute overlap is applied next time).
- **Repeat on an interval** that respects the rate limit — every 5–15 minutes is a reasonable starting point for most integrations.

**Sessions need time to settle.** A visit's counters — page views, active time, scroll depth, last page — keep changing for as long as the visitor is still on your site, and a visit is only considered over after about 30 minutes of inactivity. If you poll `GET /sessions` with `startedSince` set to a few minutes ago, you will read half-finished visits and record them as short ones. Either pin `startedBefore` to at least 30 minutes ago and treat that window as final, or re-read recent visits with `lastActiveSince` and upsert by `id`. The visitor rollups in `GET /visitors` have the same property, for the same reason.

**Run the full backfill first.** The one-per-person de-duplication described under GET /contacts is evaluated across the records in the response window. In a full backfill (no `createdSince`) that window is your entire history, so each person appears once. In an incremental poll, a person you already imported as a chat/form contact during the backfill can reappear later as an `alphie` contact if Alphie discovers them at their company after your checkpoint. Upserting by `id` keeps this safe; if you require strictly one record per person, de-duplicate by `email` on your side as well.

## GET /companies

Returns companies Alphie has identified visiting your website — via your Alphie widget or forms — ordered by most recent activity first (`lastSeenAt` descending). Traffic identified only as a bot or generic ISP, with no attributable company, is excluded automatically.

This is the same list the Companies page shows, so the totals match.

```bash
GET /api/public/v1/companies
```

### Query parameters

| Parameter | Type | Required | Default | Description |
| `page` | integer | No | `1` | Page number to retrieve. |
| `pageSize` | integer | No | `25` | Records per page. Maximum `100`. |
| `lastSeenSince` | string (ISO 8601 datetime, with timezone offset) | No | — | Only return companies whose most recent visit (`lastSeenAt`) is at or after this instant, e.g. `2026-07-01T00:00:00Z`. This is the recommended cursor for incremental syncs. |

### Response fields

Each object in `data` has the following shape:

| Field | Type | Description |
| `id` | `string` | Stable, unique identifier for the company. Use this as your dedupe/upsert key. |
| `domain` | `string` | The company's website domain. Unique per company. |
| `name` | `string | null` | Company name. |
| `website` | `string | null` | Company website URL. |
| `description` | `string | null` | Short description of the company. |
| `industry` | `string | null` | Primary industry. |
| `employeeCount` | `string | null` | Employee count, provided as a range or descriptive text where known (e.g. `"51-200"`). |
| `annualRevenue` | `string | null` | Annual revenue, provided as a range or descriptive text where known (e.g. `"$10M-$25M"`). |
| `foundedYear` | `string | null` | Year the company was founded. |
| `linkedinUrl` | `string | null` | URL of the company's LinkedIn page. |
| `city` | `string | null` | City of the company's primary location. |
| `country` | `string | null` | Country of the company's primary location. |
| `icpScore` | `number | null` | Alphie's fit score for this company against your ideal customer profile, from `0`–`100`. Higher means a better fit. |
| `icpFit` | `string | null` | Fit tier recorded when the company was last scored: `"ideal"`, `"potential"`, `"likely_not_a_fit"` or `"not_a_fit"`. `null` when no tier was recorded. |
| `sessions` | `number` | Number of identified visit sessions recorded from this company. |
| `firstSeenAt` | `string | null` | ISO 8601 timestamp of the company's first identified visit. |
| `lastSeenAt` | `string | null` | ISO 8601 timestamp of the company's most recent identified visit. |

**Note:** `icpFit` is recorded when a company is scored, not derived from `icpScore` when you read it. Alphie's score-to-tier thresholds have changed over time, so a company scored under earlier thresholds keeps the tier it was assigned then, and its `icpFit` may not match the tier its `icpScore` would produce today. Compare `icpScore` when you need one threshold applied uniformly across every row.

### Example request

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/companies?pageSize=50&lastSeenSince=2026-07-01T00:00:00Z"
```

### Example response

json

```json
{
  "data": [
    {
      "id": "8f3a1c2e-9b7d-4f10-a2c4-6e8d0b2f4a61",
      "domain": "brightloopsystems.com",
      "name": "BrightLoop Systems",
      "website": "https://www.brightloopsystems.com",
      "description": "Cloud-based inventory management software for mid-market retailers.",
      "industry": "Software",
      "employeeCount": "51-200",
      "annualRevenue": "$10M-$25M",
      "foundedYear": "2015",
      "linkedinUrl": "https://www.linkedin.com/company/brightloop-systems",
      "city": "Austin",
      "country": "United States",
      "icpScore": 87,
      "icpFit": "ideal",
      "sessions": 14,
      "firstSeenAt": "2026-06-02T14:22:31Z",
      "lastSeenAt": "2026-07-21T09:47:03Z"
    },
    {
      "id": "2b6e9d4a-1f5c-4873-b0e2-7c31da95f2c8",
      "domain": "nordvale-consulting.eu",
      "name": "Nordvale Consulting",
      "website": "https://www.nordvale-consulting.eu",
      "description": null,
      "industry": "Professional Services",
      "employeeCount": "11-50",
      "annualRevenue": null,
      "foundedYear": null,
      "linkedinUrl": "https://www.linkedin.com/company/nordvale-consulting",
      "city": "Amsterdam",
      "country": "Netherlands",
      "icpScore": 54,
      "icpFit": "potential",
      "sessions": 3,
      "firstSeenAt": "2026-07-10T08:03:12Z",
      "lastSeenAt": "2026-07-20T16:58:44Z"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 214
}
```

## GET /contacts

Returns people captured by your Alphie chat widget and forms, plus contacts Alphie has discovered at companies it has identified, ordered newest first (`createdAt` descending).

```bash
GET /api/public/v1/contacts
```

### Query parameters

| Parameter | Type | Required | Default | Description |
| `page` | integer | No | `1` | Page number to retrieve. |
| `pageSize` | integer | No | `25` | Records per page. Maximum `100`. |
| `source` | string | No | `all` | Filter by how the contact originated. One of `all`, `chat`, `form`, `alphie` — see below. |
| `createdSince` | string (ISO 8601 datetime) | No | — | Only return contacts created at or after this instant. Recommended cursor for incremental syncs. |

`source` values:

| Value | Description |
| `all` (default) | All contacts, regardless of source. |
| `chat` | Captured via your Alphie chat widget. |
| `form` | Submitted through one of your forms. |
| `alphie` | Discovered by Alphie at a company it identified visiting your site (not directly captured via chat or a form). |

**Dedupe note:** if the same person both submits a chat or form message *and* is separately discovered by Alphie at their company, they appear only once in this endpoint — as the `chat` or `form` record — never duplicated as a separate `alphie` record.

### Response fields

Each object in `data` has the following shape:

| Field | Type | Description |
| `id` | `string` | Stable, unique identifier for the contact. Use this as your dedupe/upsert key. |
| `source` | `"chat" | "form" | "alphie"` | How the contact originated. See the `source` value table above. |
| `fullName` | `string | null` | Contact's full name. |
| `firstName` | `string | null` | Contact's first name. Populated for `alphie` contacts. |
| `lastName` | `string | null` | Contact's last name. Populated for `alphie` contacts. |
| `email` | `string | null` | Contact's email address. |
| `phone` | `string | null` | Contact's phone number. Only present for `chat` and `form` contacts. |
| `jobTitle` | `string | null` | Contact's job title. Only present for `alphie` contacts. |
| `linkedinUrl` | `string | null` | URL of the contact's LinkedIn profile. |
| `message` | `string | null` | The message the visitor submitted via chat or a form. Only present for `chat` and `form` contacts. |
| `status` | `string | null` | Lead status: one of `NEW`, `CONTACTED`, `QUALIFIED`, `CONVERTED`, `LOST`. Only present for `chat` and `form` contacts; always `null` for `alphie` contacts. |
| `company` | `object | null` | The contact's associated company, or `null` if none is known. See below. |
| `createdAt` | `string` | ISO 8601 timestamp of when the contact was captured or discovered. |

The nested `company` object, when present, has the following shape:

| Field | Type | Description |
| `id` | `string` | The associated company's `id` (matches `id` in GET /companies). |
| `name` | `string | null` | Company name. |
| `domain` | `string` | Company domain. |
| `website` | `string | null` | Company website URL. |

### Example request

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/contacts?source=alphie&pageSize=50"
```

### Example response

The example below shows one contact from each `source` (a request without a `source` filter, i.e. `source=all`, would return a mix like this):

json

```json
{
  "data": [
    {
      "id": "1a4f7c2e-8b3d-4961-9c5e-d40e2a718b36",
      "source": "chat",
      "fullName": "Maria Chen",
      "firstName": null,
      "lastName": null,
      "email": "maria.chen@brightloopsystems.com",
      "phone": "+1-512-555-0148",
      "jobTitle": null,
      "linkedinUrl": null,
      "message": "Hi, we're looking for a solution to sync inventory across 3 warehouses. Can you tell me more about pricing?",
      "status": "QUALIFIED",
      "company": {
        "id": "8f3a1c2e-9b7d-4f10-a2c4-6e8d0b2f4a61",
        "name": "BrightLoop Systems",
        "domain": "brightloopsystems.com",
        "website": "https://www.brightloopsystems.com"
      },
      "createdAt": "2026-07-21T09:50:12Z"
    },
    {
      "id": "9d2e5b8a-4f1c-4036-8d7b-64c1e0af5923",
      "source": "form",
      "fullName": "Tobias Reyes",
      "firstName": null,
      "lastName": null,
      "email": "tobias.reyes@nordvale-consulting.eu",
      "phone": null,
      "jobTitle": null,
      "linkedinUrl": null,
      "message": "Requesting a demo for our operations team.",
      "status": "NEW",
      "company": {
        "id": "2b6e9d4a-1f5c-4873-b0e2-7c31da95f2c8",
        "name": "Nordvale Consulting",
        "domain": "nordvale-consulting.eu",
        "website": "https://www.nordvale-consulting.eu"
      },
      "createdAt": "2026-07-20T17:02:09Z"
    },
    {
      "id": "5f8c1a3e-9d7b-4264-a1c9-3be07f61d245",
      "source": "alphie",
      "fullName": "Priya Natarajan",
      "firstName": "Priya",
      "lastName": "Natarajan",
      "email": "priya.natarajan@brightloopsystems.com",
      "phone": null,
      "jobTitle": "VP of Operations",
      "linkedinUrl": "https://www.linkedin.com/in/priyanatarajan",
      "message": null,
      "status": null,
      "company": {
        "id": "8f3a1c2e-9b7d-4f10-a2c4-6e8d0b2f4a61",
        "name": "BrightLoop Systems",
        "domain": "brightloopsystems.com",
        "website": "https://www.brightloopsystems.com"
      },
      "createdAt": "2026-07-19T11:14:55Z"
    }
  ],
  "page": 1,
  "pageSize": 25,
  "total": 58
}
```

## GET /visitors

Returns everyone who browsed your site with the Alphie widget present and had any activity recorded, ordered by most recently active first. One row per person, not per visit.

This includes visitors who later became contacts — they carry `isContact: true` and share their `id` with GET /contacts. Placeholder records for visitors who loaded a page and left without engaging at all are excluded.

```bash
GET /api/public/v1/visitors
```

### Query parameters

| Parameter | Type | Required | Default | Description |
| `page` | integer | No | `1` | Page number to retrieve. |
| `pageSize` | integer | No | `25` | Records per page. Maximum `100`. |
| `lastSeenSince` | string (ISO 8601 datetime, with timezone offset) | No | — | Only return visitors active at or after this instant. The recommended cursor for incremental syncs. |
| `createdSince` | string (ISO 8601 datetime, with timezone offset) | No | — | Only return visitors *first seen* at or after this instant. Use this for “who is new”, as opposed to “who came back”. |

### Response fields

| Field | Type | Description |
| `id` | `string` | Stable identifier for the visitor. Use as your dedupe/upsert key. When `isContact` is true this is the same `id` the contact has in `GET /contacts`. |
| `isContact` | `boolean` | Whether this person also appears in `GET /contacts`. |
| `hasConversation` | `boolean` | Whether they exchanged messages in the chat widget. |
| `company` | `object | null` | The company Alphie identified for them, or `null`. Same shape as the `company` object in `GET /contacts`. |
| `country` | `string | null` | Country of their most recent visit. |
| `deviceType` | `string | null` | `"desktop"`, `"mobile"` or `"tablet"`, from their most recent visit. |
| `browser` | `string | null` | Browser on their most recent visit. |
| `os` | `string | null` | Operating system on their most recent visit. |
| `firstTouch` | `object` | Where they originally came from. See below. |
| `lastUrl` | `string | null` | The last page they were on. |
| `lastPath` | `string | null` | Path portion of `lastUrl`. |
| `sessionCount` | `number` | How many separate visits they have made. |
| `pageViews` | `number` | Total page views across every visit. |
| `eventCount` | `number` | Total tracked interactions across every visit. |
| `meaningfulEventCount` | `number` | As `eventCount`, excluding passive signals such as tab visibility changes and page unloads. The better engagement measure of the two. |
| `totalActiveTimeMs` | `number` | Milliseconds of *active* time — time with mouse, keyboard or touch activity, not wall-clock time with the tab open. |
| `maxScrollPct` | `number` | Deepest scroll reached on any page, 0–100. |
| `firstSeenAt` | `string | null` | ISO 8601 timestamp of their first visit. |
| `lastSeenAt` | `string | null` | ISO 8601 timestamp of their most recent activity. |
| `createdAt` | `string` | ISO 8601 timestamp of when Alphie first recorded them. |

The `firstTouch` object describes **the visit they originally arrived on**, not their most recent one:

| Field | Type | Description |
| `referrerUrl` | `string | null` | The page that linked them to your site. `null` for a direct visit, and for any referrer that was not a normal web address. |
| `referrerType` | `string | null` | One of `"direct"`, `"search"`, `"social"`, `"referral"`, `"campaign"`. Any visit carrying UTM parameters is `"campaign"`, whatever the referring page was. |
| `utmSource`, `utmMedium`, `utmCampaign`, `utmTerm`, `utmContent` | `string | null` | The UTM parameters on the URL they arrived at. |
| `landingUrl` | `string | null` | The first page of their first visit. |
| `landingPath` | `string | null` | Path portion of `landingUrl`. |

**firstTouch is taken as a whole from one visit.** If someone arrives from a Google ad in March and from LinkedIn in June, you get the March visit's referrer *and* the March visit's campaign — never a referrer from one visit paired with a campaign from another, which would describe an arrival that never happened. `landingUrl` always comes from the genuinely first visit.

**null means not recorded, not “direct”.** Alphie began recording traffic source for every visitor — rather than only for identified ones — in the release that introduced this endpoint. Visits from before then have `referrerType: null`. A visit Alphie recorded as genuinely direct is `"direct"`, not `null`, so the two are distinguishable.

### Example request

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/visitors?pageSize=50&lastSeenSince=2026-07-01T00:00:00Z"
```

### Example response

json

```json
{
  "data": [
    {
      "id": "c7d1e4b0-2f83-4a19-9c6e-1b5f7a20d834",
      "isContact": false,
      "hasConversation": false,
      "company": {
        "id": "8f3a1c2e-9b7d-4f10-a2c4-6e8d0b2f4a61",
        "name": "BrightLoop Systems",
        "domain": "brightloopsystems.com",
        "website": "https://www.brightloopsystems.com"
      },
      "country": "US",
      "deviceType": "desktop",
      "browser": "Chrome",
      "os": "macOS",
      "firstTouch": {
        "referrerUrl": "https://www.google.com/",
        "referrerType": "campaign",
        "utmSource": "google",
        "utmMedium": "cpc",
        "utmCampaign": "inventory-q3",
        "utmTerm": "warehouse sync",
        "utmContent": null,
        "landingUrl": "https://acme.com/pricing?utm_source=google&utm_medium=cpc",
        "landingPath": "/pricing"
      },
      "lastUrl": "https://acme.com/demo",
      "lastPath": "/demo",
      "sessionCount": 3,
      "pageViews": 11,
      "eventCount": 64,
      "meaningfulEventCount": 41,
      "totalActiveTimeMs": 384000,
      "maxScrollPct": 92,
      "firstSeenAt": "2026-07-02T14:22:31Z",
      "lastSeenAt": "2026-07-21T09:47:03Z",
      "createdAt": "2026-07-02T14:22:29Z"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1874
}
```

## GET /visitors/{id}

Returns one visitor and the individual visits they made.

```bash
GET /api/public/v1/visitors/{id}
```

`{id}` is the visitor's `id` from GET /visitors — and, when `isContact` is true, the same `id` the person has in `GET /contacts`.

The response is a single object with every field of a `GET /visitors` row, plus:

| Field | Type | Description |
| `sessions` | array | That visitor's visits, newest first, at most 100. Each entry has the same shape as a `GET /sessions` record. |
| `sessionsTruncated` | `boolean` | `true` when the visitor has more than 100 visits and only the newest 100 are included. Use GET /sessions to walk the rest. |

An unknown `id`, or an `id` belonging to another Alphie account, both return `404 Visitor not found`. The two are deliberately indistinguishable.

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/visitors/c7d1e4b0-2f83-4a19-9c6e-1b5f7a20d834"
```

## GET /sessions

Returns individual visits, newest first. This is the resource to aggregate for traffic reporting — visits by referrer, by campaign, by landing page, over time.

```bash
GET /api/public/v1/sessions
```

Unlike the other endpoints this one is cursor-paginated and returns no `total` — see Cursor pagination.

### Query parameters

| Parameter | Type | Required | Default | Description |
| `pageSize` | integer | No | `25` | Records per page. Maximum `100`. |
| `cursor` | string | No | — | The `nextCursor` from your previous response. Opaque — pass it back unchanged. |
| `startedSince` | string (ISO 8601 datetime, with timezone offset) | No | — | Visits that began at or after this instant (inclusive). |
| `startedBefore` | string (ISO 8601 datetime, with timezone offset) | No | — | Visits that began strictly before this instant (exclusive), so consecutive windows neither overlap nor gap. |
| `lastActiveSince` | string (ISO 8601 datetime, with timezone offset) | No | — | Visits with activity at or after this instant. Use this to re-read visits that have changed since your last poll, whenever they started. |

### Response fields

| Field | Type | Description |
| `id` | `string` | Stable identifier for this visit. Use as your dedupe/upsert key. |
| `visitorId` | `string` | The `id` of the visitor who made it — joins to GET /visitors. |
| `attribution` | `object` | Where **this visit** came from. Same shape as `firstTouch` above, but describing this visit rather than the visitor's first. |
| `country` | `string | null` | Country the visit came from. |
| `deviceType` | `string | null` | `"desktop"`, `"mobile"` or `"tablet"`. |
| `browser`, `browserVersion`, `os`, `osVersion` | `string | null` | Client software. |
| `language` | `string | null` | Browser language, e.g. `"en-GB"`. |
| `timezone` | `string | null` | IANA timezone reported by the browser, e.g. `"Europe/London"`. |
| `screenWidth`, `screenHeight` | `number | null` | Screen size in pixels. |
| `viewportWidth`, `viewportHeight` | `number | null` | Browser viewport size in pixels. |
| `connectionType` | `string | null` | Network type where the browser reports one, e.g. `"4g"`. |
| `firstUrl` | `string | null` | The page the visit started on. |
| `firstPath` | `string | null` | Path portion of `firstUrl`. |
| `lastUrl` | `string | null` | The last page of the visit. |
| `lastPath` | `string | null` | Path portion of `lastUrl`. |
| `pageViews` | `number` | Pages viewed during this visit. |
| `eventCount` | `number` | Tracked interactions during this visit. |
| `activeTimeMs` | `number` | Milliseconds of active time during this visit (see `totalActiveTimeMs` above). |
| `maxScrollPct` | `number` | Deepest scroll reached during this visit, 0–100. |
| `customContext` | `object | null` | Whatever your own site passed to the widget as context for this visit — see below. |
| `customContextTruncated` | `boolean` | `true` when `customContext` was too large to return and has been omitted. |
| `startedAt` | `string` | ISO 8601 timestamp of when the visit began. |
| `lastActiveAt` | `string` | ISO 8601 timestamp of the visit's most recent activity. |

**customContext is your own data, echoed back.** It is whatever your site supplied via the widget's `data-widget-context` attribute or `window.__alphieWidgetConfig.context`. Alphie stores and returns it verbatim and does not validate, interpret or sanitize it, so treat it as untrusted input on the way back in — exactly as you would any value that made a round trip through a browser. Values larger than 4 KB are omitted, with `customContextTruncated: true`.

### Example request

bash

```bash
curl -H "Authorization: Bearer alph_YOUR_KEY" \
  "https://q.meetalphie.com/api/public/v1/sessions?pageSize=100&startedSince=2026-07-01T00:00:00Z&startedBefore=2026-08-01T00:00:00Z"
```

### Example response

json

```json
{
  "data": [
    {
      "id": "4e91b7c2-5a08-4d6f-b3e1-9c72d0af6415",
      "visitorId": "c7d1e4b0-2f83-4a19-9c6e-1b5f7a20d834",
      "attribution": {
        "referrerUrl": "https://www.linkedin.com/feed/",
        "referrerType": "social",
        "utmSource": null,
        "utmMedium": null,
        "utmCampaign": null,
        "utmTerm": null,
        "utmContent": null,
        "landingUrl": "https://acme.com/case-studies",
        "landingPath": "/case-studies"
      },
      "country": "US",
      "deviceType": "mobile",
      "browser": "Safari",
      "browserVersion": "17.4",
      "os": "iOS",
      "osVersion": "17.4",
      "language": "en-US",
      "timezone": "America/Chicago",
      "screenWidth": 390,
      "screenHeight": 844,
      "viewportWidth": 390,
      "viewportHeight": 700,
      "connectionType": "4g",
      "firstUrl": "https://acme.com/case-studies",
      "firstPath": "/case-studies",
      "lastUrl": "https://acme.com/demo",
      "lastPath": "/demo",
      "pageViews": 4,
      "eventCount": 22,
      "activeTimeMs": 131000,
      "maxScrollPct": 78,
      "customContext": { "plan": "trial", "locale": "en-US" },
      "customContextTruncated": false,
      "startedAt": "2026-07-18T09:00:00.000Z",
      "lastActiveAt": "2026-07-18T09:31:12.000Z"
    }
  ],
  "pageSize": 100,
  "nextCursor": "eyJzIjoxNzg0MDAwMDAwMDAwLCJpIjoiYnMtOSJ9"
}
```

## Privacy and data handling

The visitor and session endpoints describe people who browsed your site, so it's worth being explicit about what they do and do not contain.

**Visitor IP addresses are never returned, because Alphie never stores them.** IPs are read from the request in memory to look up which company a visit came from, and are discarded. There is no field on any endpoint that carries one, and none can be added later without changing that policy.

**Erasing a person removes their visits too, immediately.** When you erase a lead through Alphie (GDPR Art. 17), that person disappears from `GET /visitors` and every one of their visits disappears from `GET /sessions` on the next request. There is no separate call to make and no delay. If you have already copied their records into your own system, deleting them there is your responsibility — the API cannot tell you retrospectively which ids were erased, so reconcile by absence rather than expecting a tombstone.

**Anonymising a person keeps the visit and drops the identifiers.** Anonymising is the option to use when the request is about the person rather than the visit — the aggregate stays whole. The visit keeps its counters, timing, country, device and campaign; it loses its referrer URL, its landing and exit URLs, and its `customContext`, since all four can carry identifying values in query strings. `referrerType` survives, because “arrived from search” describes nobody in particular.

**This is a server-to-server API.** `/api/public/v1` sends no CORS headers and cannot be called from a browser. Your API key grants read access to your whole account, so it must live on your server or in a secrets manager — never in client-side code, a mobile app bundle, or anything else an end user can read.

## Errors

All error responses are JSON objects with an `error` field. Some also include additional detail specific to the error.

| Status | Body | Meaning |
| `400` | `{ "error": "Invalid query parameters", "details": { … } }` | One or more query parameters failed validation (e.g. `pageSize` out of range, a malformed date, or a `cursor` that isn't one Alphie issued). `details` describes the specific problem. |
| `401` | `{ "error": "API key required" }` | No API key was supplied on the request. |
| `401` | `{ "error": "Invalid API key" }` | The supplied API key doesn't match any known key. |
| `401` | `{ "error": "API key revoked" }` | The supplied API key was valid but has since been revoked. |
| `403` | `{ "error": "Insufficient scope" }` | The API key doesn't have permission to perform this request. |
| `403` | `{ "error": "Subscription required" }` | The account's current subscription doesn't include API access. |
| `404` | `{ "error": "Visitor not found" }` | No such visitor, or the visitor belongs to another account. The two are deliberately indistinguishable. |
| `429` | `{ "error": "Too many requests", "retryAfter": 30 }` | The rate limit was exceeded. `retryAfter` is the number of seconds to wait before retrying. |
| `500` | `{ "error": "Internal server error" }` | An unexpected error occurred. Retry with backoff. |

## Versioning

The current version of the API is `v1`, identified by the `/v1` segment in the base URL. Within `v1`, changes are **additive only**: new optional query parameters and new response fields may be introduced over time, but existing fields will not be removed, renamed, or repurposed. Integrate defensively — parse responses leniently and ignore any fields you don't recognize — so future additions never break your sync.

The nested `firstTouch`, `attribution` and `customContext` objects follow the same rule: they may gain fields, so read them by key rather than by shape.

## Sitemap

Every page on this site: https://meetalphie.com/sitemap.md
