# MyVisitors
> MyVisitors: Popups, email and live chat that turn website visitors into customers. Website widgets (popups, banners, messages, social proof, tooltips, embedded forms and live chat) from one script tag; the contacts they collect; and emails that follow up.
Docs: https://notify.uptodateconnect.com/docs. The same as JSON: GET https://notify.uptodateconnect.com/api/v1.
## Add it to a website
```html
```
Once per page, in the
(or before ) of every page widgets should appear on. SITE_KEY is the site's key (public, safe in a page): Sites in the dashboard, or `you.sites` here with a key.
The script exposes `window.UTDNotify`:
- `UTDNotify.identify({ id, email, name, ...traits })`: After a login: who the visitor is. Live chat shows them, and their later form entries link to them. Traits are strings, numbers or booleans. Sent once per browser session for the same person, so calling it on every page is fine.
- `UTDNotify.reset()`: After a logout: the browser becomes a new, anonymous visitor, so the next person on it isn't linked to the one before.
- `UTDNotify.setFormData({ field: "value" }) or setFormData("widgetId", { … })`: Fills form fields by name (hidden ones too) in every widget, or one: a user id, a plan, a page's product.
- `UTDNotify.track("widgetId", "CONVERSION")`: Credits a widget with a conversion that happened outside its own form (a purchase, a sign-up). Types: CONVERSION, CLICK, IMPRESSION, CLOSE.
- `UTDNotify.reinit()`: Looks at the page again. Single-page apps are followed automatically (history changes); call it after a router swaps the whole page in a way that doesn't change the URL.
- `UTDNotify.listWidgets()`: The widgets on this page, with their form fields: for finding ids and field names in the browser console.
On the site, window.UTDNotify exists, and the request to https://notify.uptodateconnect.com/api/v1/public/sites//config?visitorId=… answers 200 (a 403 "Origin not allowed" means the page's domain isn't one of the site's allowed domains; the console then says "[MyVisitors] Failed to load config"). Widgets show once they're published in the dashboard.
## Widgets
In the dashboard (https://notify.uptodateconnect.com/app): made, designed and published there. The API lists them (GET /api/v1/widgets) but doesn't make or change them.
- `POPUP`: A popup or slide-in, often with a form.
- `BANNER`: A bar at the top or bottom of the page.
- `MESSAGE`: Live chat (a bubble), answered from the dashboard's inbox and the iPhone app; or a one-way message.
- `SOCIAL_PROOF`: Small notes of recent sign-ups or purchases.
- `TOOLTIP`: A tooltip or a multi-step tour pointing at elements on the page.
- `EMBED`: A form or card inside the page itself, in an element the page provides.
When: Straight away, after some seconds, on scroll (a percentage of the page), on exit intent (the mouse heads for the tab bar), when someone clicks an element matching a CSS selector, from what the visitor does (looks interested, gets stuck, leaves a form, goes quiet, after pages or time), or when the site's code calls UTDNotify.event(name). UTDNotify.show(widgetId) opens one from code. Where and who: Rules that must all hold: pages (the address contains, starts with or is a value; a value starting with / is a path) and pages it never shows on, devices, new or returning visitors, and conditions: where the visit came from (search, ads, social, email, another website, direct), a utm tag, the referring site, the visit number, a page seen before, another campaign seen/closed/filled in, known contact, days and hours on the workspace's clock, country, browser language. How often: How often one visitor sees it (per page view, session, visitor, day or week), between optional dates; forms can stop showing to people who already filled them in.
What the site's pages need (GET /api/v1/widgets says it per widget, as `onPage`):
- Embedded forms: An embedded form shows inside every element matching its selector (default "#utd-embed"): the page needs that element where the form belongs, for example . It waits up to 10 seconds for one (single-page apps).
- Tooltips and tours: A tooltip points at the element matching its anchor (default "[data-utd-tooltip]"); a tour, at each step's anchor in order. Those elements need the ids or attributes.
- On a click: An on-click widget opens when someone clicks an element matching its trigger's selector (anywhere on the page, also elements added later). By default that click does nothing else: a link doesn't go anywhere, a button doesn't submit.
## API
Base URL https://notify.uptodateconnect.com/api/v1. Send `Authorization: Bearer `. Made in https://notify.uptodateconnect.com/app/settings/developers (shown once), or with the "Hand it to your AI agent" prompt. Server-side only: never put a key in a web page or an app.
Key scopes:
- `contacts`: Adds and updates contacts, looks one up by email, and reads widgets and email templates. It can't list your contacts or send emails.
- `full`: Everything: contacts (read and write), widgets, templates, and sending emails (trigger, broadcasts).
JSON in and out. Errors: { error, issues? } with a 4xx status (401 no or bad key, 403 the key's scope doesn't allow it, 400 invalid input, 404 not found).
### GET /api/v1
This manifest. With a key, `you` adds the workspace: its sites (with their site keys), widgets and the key's scope.
Key: none needed.
Answers: { name, version, baseUrl, authentication, endpoints, browserSdk, webhooks, you? }
### POST /api/v1/leads
Add a contact (a lead), or update the one with the same email: `data` is merged into what's there. Safe to call on every sign-up or login. When someone changes their email, send the new one with `previousEmail`. Without `widgetId`, API contacts are filed under "Contacts from your app", a widget made for them (archived: it never shows on a site).
Key: contacts or full.
Body:
- `email`: string (email or phone is required)
- `phone`: string
- `data`: object of strings, numbers or booleans: anything you know (name, plan, company, …). Emails use it as {{key}} (or {{lead.data.key}}); `name` is also {{name}}
- `widgetId`: string, optional: the widget it came from (GET /api/v1/widgets)
- `previousEmail`: string, optional: the email they had before, when it changed (that contact gets the new one)
- `starred`: boolean, optional
```json
{ "email": "jane@acme.com", "data": { "name": "Jane Doe", "plan": "pro", "signed_up": "2026-10-18" } }
```
Answers: 201 { lead, updated: false } when added, 200 { lead, updated: true } when an existing contact was updated
### GET /api/v1/leads?email=…
The one contact with that email, or null: to check that a sign-up arrived, or what's known about someone.
Key: contacts or full.
Answers: { lead: { id, email, phone, data, starred, unsubscribed, widgetId, createdAt } | null }
### GET /api/v1/leads
The contacts, newest first.
Key: full.
Query:
- `limit`: 1–200 (default 50)
- `cursor`: nextCursor from the page before
Answers: { leads: [{ id, email, phone, data, starred, unsubscribed, widgetId, createdAt }], nextCursor }
### GET /api/v1/widgets
The widgets (popups, banners, messages, social proof, tooltips, embedded forms), with what each needs on the site's pages.
Key: contacts or full.
Answers: { widgets: [{ id, name, channel, status, siteId, createdAt, trigger, onPage }] }: trigger is "immediate", "delay", "scroll", "exit_intent", "click", "intent" (the visitor looks interested), "frustration", "form_abandon", "idle", "page_count" or "time_on_site"; onPage is what the page must have, if anything: { embedInto: "" } (an embedded form shows inside it), { pointsAt: ["", …] } (a tooltip or tour), { opensOnClick: "", clickOnlyOpens: true } (opens on a click on it; with clickOnlyOpens the click does nothing else)
### GET /api/v1/templates
The email templates, for POST /api/v1/trigger.
Key: contacts or full.
Answers: { templates: [{ id, name, subject, createdAt, updatedAt }] }
### POST /api/v1/trigger
Email a template to every subscribed contact whose `data` has all the `match` values (for example everyone who bought a product).
Key: full.
Body:
- `match`: object, at least one key: { "product_id": "abc123" } matches contacts with data.product_id = "abc123"
- `template_id`: string: an email template (GET /api/v1/templates)
- `vars`: object, optional: extra template variables for this send
```json
{ "match": { "product_id": "abc123" }, "template_id": "tpl_…", "vars": { "product_name": "Trail shoes" } }
```
Answers: { ok, matched, sent, failed }
### POST /api/v1/broadcasts/{token}/send
Send a saved broadcast now (its trigger token is on the broadcast's page in the dashboard). Each call sends a copy to its audience. Send an `Idempotency-Key` header so a retry doesn't send twice.
Key: full.
Headers:
- `Idempotency-Key`: optional, a new UUID per send: a repeat within 24 hours replays the first answer
Body:
- ``: optional: a template variable for this send
- `filter`: optional: { "productId": "…" } narrows to contacts whose data matches; { "skipIfSentWithin": "1h" } skips contacts sent this broadcast within that window (30s, 10m, 1h, 1d)
Answers: { ok, broadcastId, status, recipientCount, sentCount, failedCount, failureReason }: status "SENT" (or "FAILED") when it finished within 20 seconds, "SENDING" when the rest carries on in the background (the dashboard shows its progress; past today's email limit it goes on the next day)
## Webhooks
https://notify.uptodateconnect.com/app/settings/developers: a URL, the events, and a secret.
- `lead.created`: Someone filled in a widget's form, or a contact was added through the API
POST, JSON: { "event": "lead.created", "data": { …the contact }, "timestamp": "ISO 8601" }
- `X-UTD-Event`: the event
- `X-UTD-Signature`: sha256=
Compute the HMAC over the raw request body (before parsing it) and compare in constant time; answer 2xx within 10 seconds.