Integrations
Connect MyVisitors to your stack using API keys, outbound webhooks, and the Resend bounce webhook.
API keys
API keys let external systems authenticate against the MyVisitors REST API. Go to Developers → API keys (in the sidebar with Show everything on) and click Create a key. Give the key a name for your own reference.
The key is shown once — copy it immediately. Include it in API requests as:
Authorization: Bearer YOUR_API_KEYTreat API keys as secrets
Outbound webhooks
Webhooks let MyVisitors notify your systems when events happen. Go to Developers → Webhooks and click Add a webhook.
Supported events
- lead.created — fires every time a new contact is captured from a widget form.
Payload format
Payloads are sent as POST with Content-Type: application/json. A lead.created payload looks like:
{
"event": "lead.created",
"data": {
"id": "clxyz...",
"email": "jane@example.com",
"phone": null,
"data": { "name": "Jane", "company": "Acme" },
"widget": { "id": "...", "name": "Homepage popup" },
"createdAt": "2026-04-15T10:30:00.000Z"
}
}Signature verification
Every webhook request is signed with HMAC-SHA256 using the secret shown when you create the webhook. Verify the signature in your endpoint to ensure the request is genuine:
import { createHmac } from "crypto";
function isValidSignature(body: string, signature: string, secret: string) {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return signature === `sha256=${expected}`;
}
// In your endpoint:
const signature = request.headers["x-utd-signature"] ?? "";
const rawBody = await request.text();
if (!isValidSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return new Response("Forbidden", { status: 403 });
}Retries
If your endpoint returns a non-2xx status code, MyVisitors will retry the delivery up to 3 times with exponential backoff. All delivery attempts (including failures) are recorded under the webhook in Developers → Webhooks.
Sending one-off emails via API
In the API a one-off email is a broadcast. Any saved broadcast can be re-fired from your own backend by hitting its trigger token. Each call clones the source broadcast (template + audience), sends the clone immediately, and returns delivery counts. The clone is linked to the source via triggeredById, so every fire shows up under History in Emails → One-off.
Find the trigger token under API trigger on the one-off email in Emails → One-off. Tokens look like btr_M_V06f4PvAEze6cqfxjkKSOmGHmSmFu5.
Endpoint
POST https://notify.uptodateconnect.com/api/v1/broadcasts/{token}/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonBody (all fields optional)
- Any top-level scalar key becomes a per-send template variable. Use it to render announcement-level values like product name, CTA url, coupon code, etc. inside
{{...}}placeholders in the email template. filter(object) — narrows recipients by matching againstlead.data.*. Recipients must match the broadcast's saved audience AND every key infilter.- Scalar value — exact equality.
{ "productId": "abc" }→ contacts withdata.productId === "abc". - Array value — match any (OR within the key).
{ "tag": ["vip", "beta"] }→ contacts whosedata.tagequals anyof those values. Useful for "send to anyone tagged X or Y". - Multiple top-level keys AND together (all must match).
- Scalar value — exact equality.
Example
Send a one-off announcement to the slice of your audience that matches a value stored on the contact. The body fields are arbitrary — name them whatever your template uses.
POST /api/v1/broadcasts/btr_yourTokenHere/send
{
"headline": "Your update is ready",
"ctaUrl": "https://example.com/details",
"filter": { "segment": "newsletter" }
}With this body the system selects contacts whose data.segment === "newsletter"(intersected with the broadcast's saved audience) and passes headline and ctaUrl through to the template. Reference them with their key wrapped in {{ }}:
Subject: {{headline}}
Hi {{lead.name}},
{{headline}} — read the details here:
{{ctaUrl}}Combining API vars with per-contact values
Every recipient also has the variables built from their own contact record available in the same template — there's no "API vs. contact" split, they live in the same dictionary. So a single template can mix announcement-level values from the API call with personalized values from the contact.
The per-contact variables you can reference:
{{lead.name}}— derived fromlead.data.name,lead.data.firstName, or the contact's email; falls back to"there".{{lead.email}},{{lead.phone}},{{lead.id}}.{{lead.widget}}— the name of the widget that captured the contact.{{lead.data.X}}— any custom field stored on the contact, whereXis its field name (e.g.{{lead.data.productId}}).{{name}},{{email}}— convenience aliases forlead.nameandlead.email.
Filtering by contact data and personalizing with it at the same time is the common case — e.g. target everyone in a segment, then address each by name and reference the value that put them in scope:
POST /api/v1/broadcasts/btr_yourTokenHere/send
{
"headline": "A new update for {{lead.data.segment}} members",
"ctaUrl": "https://example.com/details",
"filter": { "segment": "newsletter" }
}API vars are not interpolated
{{lead.data.segment}} inside them are not expanded. Put the per-contact placeholders in the template body, not in the API payload.A template that mixes both kinds of variables:
Subject: {{headline}}
Hi {{lead.name}},
You're in the {{lead.data.segment}} segment, and we just shipped something we
think you'll like.
{{headline}} — read the details here:
{{ctaUrl}}
Thanks,
The {{lead.widget}} teamVariable precedence
API-supplied variables overrideper-contact values when both define the same key — they represent the caller's deliberate per-send intent. If a contact happens to have a headline in its own data, the API value wins for this send. To avoid ambiguity, prefer {{lead.data.X}} (the fully-qualified form) when you specifically want the per-contact value.
Response
{
"ok": true,
"broadcastId": "cmclone...",
"recipientCount": 42,
"sentCount": 42,
"failedCount": 0,
"failureReason": null
}Filter caveats
contains, no substring/regex, and no support yet for filtering when the contact stored its own array (e.g. data.tags: ["a", "b"]). If you typo a key or pass a value no contact has, the call succeeds with recipientCount: 0. Outside of filter, nested objects and arrays in the body are ignored as template variables — only string-ifiable scalars are accepted there.Skip recently-sent contacts
Some integrations fire the same trigger repeatedly in quick succession — e.g. a Shopify back-in-stock webhook firing every time inventory blips above zero. To stop the same contact getting the same broadcast twice in a few minutes, set filter.skipIfSentWithin to a duration. Contacts who received this broadcast inside the window are excluded from the new send; everyone else gets it as normal.
POST /api/v1/broadcasts/btr_yourTokenHere/send
{
"productName": "Espresso machine",
"ctaUrl": "https://example.com/p/espresso",
"filter": {
"productId": "p_espresso",
"skipIfSentWithin": "10m"
}
}Accepted units: s, m, h, d — e.g. "30s", "10m", "1h", "24h", "1d". Malformed values return 400. Omit the field entirely → no dedupe.
- Dedupe is per source broadcast. All trigger fires of the same saved broadcast share one stream, so a follow-up call 5 minutes later sees the first call's recipients. Different broadcasts (different trigger tokens) track separately, so a stock alert won't suppress your weekly digest.
- The window is enforced against actual sends, not filter matches. If a contact was in the recipient set but bounced or unsubscribed before delivery, they aren't blocked from later sends.
- Combine freely with
Idempotency-Key— the two solve different problems.Idempotency-Keyreplays the same response for retries of one logical call;skipIfSentWithindedupes across different logical calls.
Idempotency
Network retries, double-clicks, and server-side retry loops can easily fire the same broadcast twice — sending the same email to every recipient a second time. To protect against this, pass a unique Idempotency-Key header per logical send. Generate a fresh UUID (or any stable string) on your end for each new send event; replays with the same key within 24 hours return the original response without re-sending.
POST /api/v1/broadcasts/btr_yourTokenHere/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Idempotency-Key: 7f1a2b3c-4d5e-4a6b-9c7d-8e9f0a1b2c3d
{
"headline": "Your update is ready",
"ctaUrl": "https://example.com/details",
"filter": { "segment": "newsletter" }
}- First call with that key — sends as normal, caches the response.
- Replay with the same key within 24h — returns the cached response (same
broadcastId,sentCount, etc.) and adds anIdempotency-Replayed: trueresponse header. Nothing is sent. - Replay while the original is still in-flight — returns
409 ConflictwithRetry-After: 5so you can back off and try again. - New key with the same body — treated as a deliberate fresh send. Use a new UUID when you genuinely want to re-fire the same announcement (e.g. a corrected version).
No key = no protection
The loader script
The loader script (/loader.js) is the JavaScript snippet you embed on your website. It bootstraps the MyVisitors SDK and polls the config API to decide which widgets to display.
Key behaviors:
- Fetches your published widgets from
/api/v1/public/configusing yoursiteKey. - Evaluates each widget's targeting and trigger conditions in the visitor's browser.
- Injects the widget UI into the DOM when conditions are met.
- Submits form data to
/api/v1/public/leadsto create a contact. - Initialises the chat widget and connects to
/api/v1/public/chat/*when a MESSAGE widget is active.
CORS and allowed domains
JavaScript API
Once the loader runs it exposes a small API on window.UTDNotify. Use these methods to customize widget behavior from your own code.
UTDNotify.setFormData(values) — global
Pre-fills or overrides values for any form field by field name. This is the recommended way to populate hidden fields with per-visitor context (tenant ID, plan, referrer, logged-in user info), and works equally well for visible fields you want to pre-fill as defaults.
The single-argument form applies to every widget on the page that has a field with the matching name:
<script async src="https://notify.uptodateconnect.com/loader.js" data-site="pk_..."></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (window.UTDNotify) {
window.UTDNotify.setFormData({
tenant_id: "acme-co", // overrides any hidden field named "tenant_id"
plan: "pro", // any custom hidden field
email: "user@example.com", // pre-fills any visible Email field
});
}
});
</script>UTDNotify.setFormData(widgetId, values) — targeted
When you have multiple widgets on the same page (e.g. a popup and an embedded form that share a field name like email), pass the widget ID as the first argument to scope the values to that widget only.
window.UTDNotify.setFormData("cmovhx30s0001prrwzfbifb6m", {
source: "newsletter-page",
email: "user@example.com",
});
// You can call it once per widget — values don't bleed across widgets.
window.UTDNotify.setFormData("cmpopup1234", { campaign_tag: "spring-sale" });Targeted values override global values when both are set for the same field on the same widget.
Finding a widget's ID
/app/editor/cmovhx30s.... For a quick discovery from the browser console on a live page, run UTDNotify.listWidgets() — it returns one object per registered widget, with itsid, name, channel, and form fields.UTDNotify.listWidgets()
Returns metadata for every widget the loader has registered on the page — its ID, name, channel, variant, and form fields (name, type, label, required). Useful for discovering widget IDs and field names from devtools before wiring up setFormData.
UTDNotify.listWidgets();
// → [
// {
// id: "cmovhx30s0001prrwzfbifb6m",
// name: "Fritel Out of Stock Embed",
// channel: "embed",
// variant: "form",
// fields: [
// { name: "email", type: "email", label: "Email", required: true },
// { name: "name", type: "text", label: "Name", required: false },
// { name: "product_id", type: "hidden", label: "Product id", required: false },
// ],
// },
// ]Sending arbitrary contact metadata without a hidden field
Keys passed to setFormData()that don't match any input in the form are still attached to the submitted contact's data JSON. This lets you tag contacts with per-visitor context (campaign, source, product id, etc.) without declaring a hidden field for each one.
// No hidden field named "tag" needs to exist — it just lands in lead.data.tag.
window.UTDNotify.setFormData("cmovhx30s0001prrwzfbifb6m", {
tag: "product1",
campaign: "winter-2026",
});Tag chips vs. lead.data
lead.data(free-form JSON) — visible when you open the contact's detail panel. They do not create colored tag chips in the Contacts table; those use a separate Tag model assigned manually or via automatic emails.How values resolve at submit time
- What the visitor typed in a visible input — always wins.
- Targeted value for that widget from
setFormData(widgetId, ...). - Global value from
setFormData(values). - Static value set on a hidden field in the widget editor.
Keys with no matching form input are merged into the submitted data after the inputs are collected — they never overwrite a value the visitor typed.
Match the field name exactly
nameattribute of the field as configured in the editor — not the label. The field name is shown in each field's Advanced section in the editor. For example, a hidden field labelled Tenant ID with name tenant_id is set with { tenant_id: "..." }, not { "Tenant ID": "..." }.You can call setFormData() before or after the widget renders. Already-rendered inputs are patched in place, so it works fine inside DOMContentLoaded, after an async user fetch, or any time the visitor's identity becomes known.
UTDNotify.identify(user)
Associates the current visitor with a known user record (e.g. after they log in). Pass an object with any combination of id, email, name, phone, and custom traits. Later contacts from this visitor will be linked to the same identified record.
window.UTDNotify.identify({
id: "user_123",
email: "jane@acme.com",
name: "Jane Doe",
plan: "pro",
});It's sent once per browser session for the same person, so calling it on every page is fine.
UTDNotify.event(name)
Says something happened on your site, for a campaign set to show When your site sends an event. Names use letters, numbers, dots, dashes and underscores. An event sent before the campaign loaded still counts on the same page.
// After the first step of your quote form
window.UTDNotify.event("quote-started");UTDNotify.show(widgetId)
Opens a campaign now, from your own code, for example from a “Get a quote” button. It still keeps to the campaign's rules and how often it shows: one that can't show on this page does nothing.
document.querySelector("#quote").addEventListener("click", () => {
window.UTDNotify.show("cmovhx30s0001prrwzfbifb6m");
});UTDNotify.reset()
Call it when someone logs out: the browser becomes a new, anonymous visitor, so the next person on a shared computer isn't linked to the one before.
UTDNotify.track(widgetId, eventType)
Records an analytics event for a specific widget. Useful when you want to attribute a conversion that happens outside of the widget's own form. eventType is"CONVERSION" (the default), "CLICK", "IMPRESSION" or "CLOSE".