MenuServer-side events

Installation guides

Server-side events

Send events from your backend to POST /api/v1/events with an API key, when an action happens on the server rather than in the browser.

Some events happen on your server: a trial that converts from a background job, a signup through your API, an action in a mobile app backend. Send these to POST https://api.productlense.app/api/v1/events with an API key. The body is the same as the browser script sends, plus the visitor's IP address and user agent so the event joins the same visitor as their browser activity.

Before you start

  • Server-side events need an API key, which is part of the Scale plan. An owner or admin creates it in Settings → API keys with the events scope. The key starts with mk_live_ and is shown once; store it as a secret, never in browser code.
  • The browser script is still the main source of pageviews. Server-side events add to it.

Send an event

curl -X POST https://api.productlense.app/api/v1/events \
  -H "Authorization: Bearer $PL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"s":"SITE_ID","e":"Trial converted","u":"https://example.com/app/billing",
       "uid":"user_123","p":{"plan":"growth"},
       "ip":"203.0.113.7",
       "ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15"}'
const res = await fetch('https://api.productlense.app/api/v1/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    s: process.env.PL_SITE_ID,
    e: 'Trial converted',
    ip: req.ip, // the visitor's address, from the original request
    ua: req.get('user-agent'),
    u: 'https://example.com/app/billing',
    uid: user.id,
    p: { plan: 'growth' },
  }),
});
if (res.status !== 204) {
  const body = await res.json();
  throw new Error(`ProductLense rejected the event: ${body.error.code}: ${body.error.message}`);
}
import os
import requests

res = requests.post(
    "https://api.productlense.app/api/v1/events",
    headers={"Authorization": f"Bearer {os.environ['PL_API_KEY']}"},
    json={
        "s": os.environ["PL_SITE_ID"],
        "e": "Trial converted",
        "u": "https://example.com/app/billing",
        "ip": request.remote_addr,  # the visitor's address
        "ua": request.headers.get("User-Agent", ""),
        "uid": user.id,
        "p": {"plan": "growth"},
    },
    timeout=10,
)
if res.status_code != 204:
    err = res.json()["error"]
    raise RuntimeError(f"ProductLense rejected the event: {err['code']}: {err['message']}")
package productlense

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type Event struct {
	Site   string            `json:"s"`
	Name   string            `json:"e"`
	URL    string            `json:"u"`
	UserID string            `json:"uid,omitempty"`
	IP     string            `json:"ip,omitempty"`
	UA     string            `json:"ua,omitempty"`
	Props  map[string]string `json:"p,omitempty"`
}

var client = &http.Client{Timeout: 10 * time.Second}

func Send(ev Event) error {
	body, err := json.Marshal(ev)
	if err != nil {
		return err
	}
	req, err := http.NewRequest(http.MethodPost, "https://api.productlense.app/api/v1/events", bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PL_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusNoContent {
		return fmt.Errorf("productlense: events API returned %s", res.Status)
	}
	return nil
}

A successful request returns 204 No Content with an empty body. Errors use the JSON envelope described in errors.

Rules

  • s must be the site the API key belongs to; any other site id is refused with 403 forbidden.
  • e is pageview, $identify, or a custom name of 1 to 64 letters, digits, spaces, underscores or hyphens.
  • u is required: an absolute http or https URL whose hostname is one of the site's hostnames. Use the page the action relates to.
  • ip and ua are optional and accepted only on this endpoint. Without them, the event is counted against your server's own address and user agent, which joins it to none of your visitors and may be dropped as a bot (user agents such as curl/, python-requests or Go-http-client are filtered). Pass the visitor's values from the original request.
  • uid attaches your user id, up to 128 bytes. p is a JSON object of up to 2 KB.
  • ts is optional, in milliseconds. A time more than an hour in the past or more than a minute in the future is replaced by the time ProductLense receives the event.
  • Send up to 50 events in one request with { "s": "SITE_ID", "b": [ {...}, {...} ] }. Every event in a batch belongs to the same site. The whole body is limited to 8 KB.
  • Every event except $identify counts toward your plan's monthly events.

See the Events API reference for every field and status code, and the Payments API for sending payments.

Verify your installation

  1. Send one test event as above and check that the response status is 204.
  2. Open the Live page in the ProductLense dashboard. The event appears within a few seconds, and under Goals once you add its name as a goal.
  3. If you get 400, read the message: it names the field that failed, for example hostname not allowed for this site.

Requests without ip and ua from a server user agent are dropped as bots without an error. If events do not appear, follow Verify your installation.

    ↑ ↓ to move, Enter to open