# enso Signal API - visitor and prospect touches

Read every touch a person or an account has with enso.bot and feed it into an
outreach system. Signals cover form fills, website URL submissions, content
reads and unlocks, newsletter subscribes, wizard progress, Slack requests,
briefing registrations, email opens and clicks, and de-anonymized visits.

Base URL: `https://www.enso.bot/api/public/v1/signals`
Auth: header `x-api-key: <SIGNALS_API_KEY>` (or `Authorization: Bearer <key>`) on every request.
Read-only. All responses are `application/json`, never cached.

## Endpoints

| Method | Path | What it returns |
| --- | --- | --- |
| GET | `/api/public/v1/signals` | flat, newest-first event stream across every source |
| GET | `/api/public/v1/signals/accounts` | the same events rolled up per company domain |
| GET | `/api/public/v1/signals/accounts/{domain}` | one account: summary, people, full timeline |
| GET | `/api/public/v1/signals/people/{email}` | one person: summary plus full timeline |

`{domain}` accepts `acme.com`, `www.acme.com` or `https://acme.com/pricing` - it is
normalized. `{email}` must be URL-encoded.

## Query parameters

| Param | Applies to | Default | Notes |
| --- | --- | --- | --- |
| `since` | all | 90 days ago | ISO timestamp; poll with the `next_since` of the last response |
| `until` | all | none | ISO timestamp upper bound |
| `days` | all | - | shortcut for `since = now - N days` (max 730) |
| `limit` | all | 100 (events), 100 (accounts) | max 1000 events, 500 accounts |
| `type` | all | - | comma separated event types or source names |
| `domain` | `/signals` | - | filter the stream to one company domain |
| `email` | `/signals` | - | filter the stream to one person |
| `q` | `/signals` | - | substring match on email, name, company, domain, path |
| `stage` | `/signals/accounts` | - | `lead`, `engaged` or `visitor` |

## Signal object

```json
{
  "id": "book_a_call:2f1c...",
  "type": "book_a_call_submitted",
  "source": "book_a_call",
  "occurred_at": "2026-08-25T09:14:02.000Z",
  "email": "dana@acme.com",
  "name": "Dana Levi",
  "company": "Acme",
  "domain": "acme.com",
  "page_path": "/book-a-call",
  "url": "https://www.enso.bot/book-a-call",
  "session_id": null,
  "meta": { "monthly_budget": "25k-50k", "interest": "agentic-seo" }
}
```

`id` is stable and globally unique (`source:row_id`), so it doubles as an
idempotency key on your side. `domain` is the account key: it comes from the
submitted company URL when there is one, otherwise from the work-email domain.
Free inboxes (gmail, outlook, ...) never produce a domain.

## Sources and event types

| Source | Event type(s) | Trigger |
| --- | --- | --- |
| `book_a_call` | `book_a_call_submitted` | Book a call form |
| `site_submission` | `website_url_submitted` | website URL typed on the homepage / blog / campaign inputs |
| `growth_plan` | `growth_plan_requested` | growth plan wizard completion |
| `chatgpt_credits` | `chatgpt_credits_applied` | ChatGPT ads credits wizard |
| `funnel` | `funnel_view`, `funnel_click`, `funnel_form_start`, `funnel_form_submit`, `funnel_form_success`, ... | CTA and form funnel tracking |
| `engagement` | `content_hack_read`, `content_category_view`, `content_email_click`, `content_unlock` | reading behavior |
| `research_reader` | `research_unlocked` | email-gated research unlock |
| `identified_reader` | `anonymous_visitor_identified` | de-anonymized visitor |
| `subscriber` | `newsletter_subscribed` | newsletter signup |
| `slack_channel` | `slack_chat_requested` | Chat on Slack CTA |
| `briefing` | `briefing_registered` | live briefing registration |
| `email` | `email_delivered`, `email_opened`, `email_clicked`, ... | email engagement |
| `wizard` | `wizard_in_progress`, `wizard_completed` | partially filled wizards (abandonment) |

## Account object

```json
{
  "domain": "acme.com",
  "company": "Acme",
  "first_seen_at": "2026-08-01T10:00:00.000Z",
  "last_seen_at": "2026-08-25T09:14:02.000Z",
  "signal_count": 14,
  "stage": "lead",
  "sources": ["site_submission", "engagement", "book_a_call"],
  "types": ["website_url_submitted", "content_hack_read", "book_a_call_submitted"],
  "pages": ["/", "/blog/wikipedia-castle-experiment", "/book-a-call"],
  "people": [
    { "email": "dana@acme.com", "name": "Dana Levi", "last_seen_at": "2026-08-25T09:14:02.000Z", "touches": 9 }
  ]
}
```

`stage`: `lead` (submitted a high-intent form), `engaged` (known email, no form yet),
`visitor` (anonymous or URL-only touch).

## Responses

- `200` - `{ ok: true, ... }`
- `400` - `invalid_domain` / `invalid_email`
- `401` - `unauthorized` (missing or wrong key)

Account and person lookups always return `200` with `seen: false` when there is
no history, so they are safe to call as an enrichment check.

## Examples

Poll the stream every 5 minutes:

```bash
curl -sS "https://www.enso.bot/api/public/v1/signals?since=2026-08-25T00:00:00Z&limit=500" \
  -H "x-api-key: $SIGNALS_API_KEY"
```

Store `next_since` from the response and pass it as `since` on the next call.

Check whether a prospect touched the site during their journey:

```bash
curl -sS https://www.enso.bot/api/public/v1/signals/accounts/acme.com \
  -H "x-api-key: $SIGNALS_API_KEY"

curl -sS https://www.enso.bot/api/public/v1/signals/people/dana%40acme.com \
  -H "x-api-key: $SIGNALS_API_KEY"
```

Accounts that filled a high-intent form in the last 7 days:

```bash
curl -sS "https://www.enso.bot/api/public/v1/signals/accounts?days=7&stage=lead" \
  -H "x-api-key: $SIGNALS_API_KEY"
```

Only URL submissions and reads:

```bash
curl -sS "https://www.enso.bot/api/public/v1/signals?type=website_url_submitted,engagement&days=14" \
  -H "x-api-key: $SIGNALS_API_KEY"
```

## Integration notes

- Poll on `since` + dedupe on `id`; the stream is newest-first.
- `limit` caps the merged result, so use a tight `since` window for backfills.
- Signals are read-only intent data. Respect the consent rules that apply to
  the list you push them into - a website touch is not marketing consent.
- Wiring up an autonomous agent? Read the operating manual at
  [`/signals-agent.md`](https://www.enso.bot/signals-agent.md): polling loop,
  intent ranking, allowed actions and the enso voice rules for drafted copy.
