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
eventsscope. The key starts withmk_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
smust be the site the API key belongs to; any other site id is refused with403 forbidden.eispageview,$identify, or a custom name of 1 to 64 letters, digits, spaces, underscores or hyphens.uis required: an absolutehttporhttpsURL whose hostname is one of the site's hostnames. Use the page the action relates to.ipanduaare 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 ascurl/,python-requestsorGo-http-clientare filtered). Pass the visitor's values from the original request.uidattaches your user id, up to 128 bytes.pis a JSON object of up to 2 KB.tsis 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
$identifycounts 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
- Send one test event as above and check that the response status is
204. - 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.
- If you get
400, read the message: it names the field that failed, for examplehostname 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.