Skip to content
Browse documentation

Analytics & events

What store events NanoSite sends, to which destinations, and how to integrate when you run your own storefront frontend. This is the "outbound" side referenced from Webhooks & events.

Overview

Your store emits a canonical set of events once, and we fan them out to every destination you enable: Klaviyo, Facebook, Google Tag Manager, Mixpanel, Twilio, or your own Webhook URL. Two delivery paths work together:

  • Server-side (the backbone) — authoritative events (orders, fulfillment, customer sign-ups) are emitted by our backend and delivered to each destination's server API. This works no matter what frontend your store uses, and survives ad-blockers.
  • Client-side — for storefronts we build for you, a small browser layer sends in-page events (page/product views, funnel steps) directly to your destinations.

Configure destinations per store under Add-ons → Marketing. An event that fires both client- and server-side carries a shared id so it is counted once.

Destinations

Destination Server Client Config
Klaviyo Events + Profiles API Onsite JS Private API key, public key
Facebook (Meta) Conversions API Pixel Pixel ID, CAPI token
Google Tag Manager Server container (optional) Web container (dataLayer) Web container ID, server URL
Mixpanel Track API Browser SDK Project token, API host
Twilio SMS (transactional/marketing) — Account SID, auth token, from number
Webhook Signed JSON POST to your URL — Endpoint URL, signing secret

Secrets are encrypted at rest and never returned to the browser. Public identifiers (a Mixpanel project token, a Facebook pixel id) are exposed through the storefront public config so a browser SDK can use them.

Events out of the box

These common events are emitted automatically for every store — server-side, from our verified checkout / order / customer code — so they are trustworthy by construction:

Event Fires when Key properties
order_placed Payment captured order id, currency, value, items
order_fulfilled Order marked fulfilled order id, items
order_refunded Order refunded order id, value
order_cancelled Order cancelled order id
customer_registered A shopper creates an account (sign-up) email, external id, name, method
customer_identified An existing shopper signs in email, external id, name
newsletter_subscribed Subscribe form submitted email, list

customer_registered is your sign-up event. It fires once per new account, whichever way the account was created. method says which way: password, google, magic_link or otp. Each destination receives it as its own registration event:

Destination Sign-up is sent as
Facebook (Meta) CompleteRegistration
Google Tag Manager (GA4) sign_up (with method)
Mixpanel customer_registered (with method)
Klaviyo Profile created + Registered Account metric

The following browsing events are also part of the taxonomy and may be sent from a storefront (see custom-frontend integration): product_viewed, product_added_to_cart, checkout_started, customer_identified. Authoritative order_* and customer_registered events are server-only and cannot be sent from a browser (so they can never be spoofed with a public key).

Custom events

Beyond the common taxonomy, a store can send custom events unique to its funnel (for example a nutrition brand's assessment_completed, formula_generated, or welcome_kit_dispatched). A custom event carries a customName and a flat props object and flows through the same pipeline to every enabled destination.

{
  "name": "custom",
  "customName": "formula_generated",
  "props": { "formula_version": "v2", "macro_focus": "energy" },
  "profile": { "externalId": "cus_123" },
  "consent": { "analytics": true }
}

Webhook destination

Use the Webhook destination to send store events to a system you run yourself: your own backend, a data warehouse loader, or an automation tool such as Zapier, Make or n8n. Requests follow the Standard Webhooks spec, so you can verify them with an off-the-shelf library in most languages.

Enable it under Add-ons → Marketing → Webhook. You only enter one thing:

  • Endpoint URL: a public https:// URL. Plain http, IP addresses and internal hostnames are rejected, and redirects are not followed.

When you save, we create a signing secret for you (whsec_…). Use Copy to paste it into your endpoint, or into Zapier, Make or n8n, so it can check that requests come from us. Reveal shows it again at any time. Regenerate replaces it; the old secret stops working right away, so update your endpoint at the same time. The secret is encrypted at rest.

Use Send test event to check the endpoint. It POSTs a nanosite_test_event right away.

Which events you receive

Every event that reaches the pipeline is forwarded, but not every event is emitted automatically:

  • Always sent: order_placed, order_fulfilled, order_refunded, order_cancelled, customer_registered, customer_identified (on sign-in) and newsletter_subscribed. Our backend emits these itself.
  • Only if your storefront sends them: product_viewed, product_added_to_cart, checkout_started and custom. Only the browser sees these actions, so they reach the webhook only when your storefront reports them with trackEvent (see Custom frontend integration).

What we send

Each event is a POST with a JSON body:

{
  "id": "mevt_01J…",
  "type": "order_placed",
  "version": 1,
  "timestamp": "2026-09-30T12:00:00.000Z",
  "data": {
    "profile": { "customerId": null, "email": "[email protected]", "phone": null, "name": null },
    "consent": { "marketing": true, "analytics": true, "region": null, "source": "checkout" },
    "order": { "id": "order_…", "status": "paid", "currencyCode": "USD", "total": 2500, "items": [] }
  }
}

data depends on type. Every field of every event type, with a full example, is listed in the generated Webhook events reference. version is the payload version: it changes only for a breaking change, so ignore fields you don't recognize (see Versioning).

Headers:

Header Value
webhook-id The event id. It stays the same across retries, so use it to deduplicate.
webhook-timestamp Unix seconds when the request was signed
webhook-signature v1, + base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>

Responding and retries

Return any 2xx status to acknowledge an event. A timeout (8 s), a redirect or any other status counts as a failure, and the event is retried with exponential backoff: 8 attempts over about four hours. Retries can deliver the same event twice, so deduplicate on webhook-id.

Verifying the signature

Verify against the raw request body, before any JSON parsing. The simplest way is the Standard Webhooks library (npm install standardwebhooks, also available for Python, Go, Ruby, PHP, Java and others). It checks the signature and rejects timestamps more than 5 minutes old:

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.NANOSITE_WEBHOOK_SECRET); // the "whsec_…" secret you copied
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
}); // throws if invalid

Without the library:

import { createHmac, timingSafeEqual } from "node:crypto";

const verify = (rawBody: string, headers: Record<string, string>, secret: string): boolean => {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // older than 5 minutes
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
  // The header is a space-separated list of "v1,<signature>" entries.
  return (headers["webhook-signature"] ?? "").split(" ").some((entry) => {
    const [version, sig] = entry.split(",");
    return version === "v1" && sig?.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
};

Consent

Webhook delivery is not gated on consent. The endpoint is your own system, and order events often drive operational work (fulfillment, accounting) that must not depend on a marketing opt-in. Every event carries the shopper's consent object instead. Check it before using the data for marketing or passing it on to a third party.

Client vs server

Server is the backbone: our backend emits authoritative events and forwards them to each destination's server API. It is independent of your frontend and cannot be tampered with by a browser. Client events (page/product views, in-page funnel steps) are only observable in the browser; for storefronts we build, we send them for you. If you run your own frontend, send them yourself (below). A shared eventId deduplicates an action reported from both sides.

Custom frontend integration

If you build your own storefront frontend, we don't control that client — so integrate through the SDK. Send browsing / custom events to us and we forward them to every enabled destination:

import { trackEvent } from "@nanosite/ecommerce-sdk";

// A common browsing event
await trackEvent({
  name: "product_added_to_cart",
  line: { title: "Tee", quantity: 1, unitAmount: 2500, currencyCode: "USD" },
  consent: { analytics: true },
});

// A custom funnel event
await trackEvent({
  name: "custom",
  customName: "assessment_completed",
  props: { assessment_variant: "v1" },
  profile: { externalId: customerId },
  consent: { analytics: true },
});

Pass a stable eventId if you also fire the same action via a pixel, so it dedups. Authoritative order_* and customer_registered events are emitted server-side and are rejected here.

To also run a browser SDK yourself (e.g. Mixpanel), read the public token from GET /store/marketing/destinations and initialize on your side:

const res = await fetch("/api/store/marketing/destinations", {
  headers: { "x-publishable-key": PUBLISHABLE_KEY },
});
const { data } = await res.json();
const mp = data.find((d) => d.id === "mixpanel");
if (mp?.enabled) mixpanel.init(mp.config.projectToken, { api_host: mp.config.apiHost });

Every event carries a consent signal (marketing / analytics). Consent is enforced server-side: a destination that requires marketing consent (e.g. Klaviyo) or analytics consent (e.g. Mixpanel, GTM) is skipped when that consent is absent — regardless of what a client sends. The Webhook destination is the exception. It receives every event along with its consent object, and your endpoint applies it. Capture the shopper's consent at checkout / your consent banner and pass it with each event.

Sign-up and sign-in are no exception. The register, Google, magic-link and one-time-code endpoints accept an optional consent object, which is attached to the customer_registered / customer_identified event. If it is omitted, the event is recorded with consent denied, so any destination that requires consent skips it. Creating an account does not count as consent by itself.

await registerCustomer({
  email,
  password,
  consent: { marketing: bannerState.marketing, analytics: bannerState.analytics, source: "signup" },
});