Server-Side Tracking
Send pageviews and events from your backend when the browser can't (or shouldn't).
Business+
When to use it
The browser tracker handles most of what you need. Reach for the server SDK when the event happens outside the browser:
- Stripe webhook deliveries — track subscription created, payment failed, cancelled
- Cron jobs and background workers — nightly exports, data syncs, email sends
- Mobile or native apps — no DOM means no browser tracker
- Server-rendered API clients — where the request doesn't originate from a browser
Install
npm install produl-serverCreate a server key
- 1
Open the site's Settings
In your dashboard, navigate to the site you want to track and open Settings → Server keys.
- 2
Create a key
Click Create server key, give it a label (e.g., production-api), and copy the resulting secret. It's shown only once — store it in your secret manager or environment variables immediately.
- 3
Set the environment variable
bashPRODUL_SERVER_KEY=sk_••••••••••••••••••••••••
Treat server keys like passwords
Usage
import { Produl } from 'produl-server'
const produl = new Produl({
serverKey: process.env.PRODUL_SERVER_KEY!,
})
export async function handleInvoicePaid(event: StripeEvent) {
await produl.trackEvent('invoice_paid', {
amount_cents: event.data.object.amount_paid,
currency: event.data.object.currency,
plan: event.data.object.lines.data[0]?.plan?.nickname,
}, {
visitorId: event.data.object.customer,
})
}The Produl class exposes two methods:
| Method | Signature | Notes |
|---|---|---|
trackPageview | (data: PageviewData) => Promise<void> | Requires path and visitorId. |
trackEvent | (name, properties?, data?) => Promise<void> | Requires a name. data.visitorId recommended. |
flush | () => Promise<void> | Force-send any queued events. |
PageviewData
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Normalized path (e.g. /pricing) |
visitorId | string | yes | Stable identifier for the visitor |
url | string | no | Full URL, defaults to https://server{path} |
title | string | no | Page title |
referrer | string | no | Referrer URL |
sessionKey | string | no | Groups pageviews into a session |
Batching & flushing
The client buffers events and flushes them to the API in one of three cases:
- The queue reaches 10 events
- 5 seconds pass since the last enqueue
flush()is called explicitly
The SDK also installs a beforeExit listener on Node so queued events are sent before the process exits. In serverless environments (Vercel, Lambda), call await produl.flush() before the handler returns to avoid losing the last batch.
export async function POST(req: Request) {
const body = await req.json()
await produl.trackEvent('signup', { plan: body.plan }, { visitorId: body.userId })
await produl.flush() // flush before returning
return Response.json({ ok: true })
}Rate limits
Server-authenticated requests are rate-limited to 2,000 requests per minute per IP — a 10× bump over the default client-side limit of 200 req/min. Requests above the limit receive 429 Too Many Requests; the SDK retries them on the next flush.
Using fetch directly
If you're on a language without an official SDK, you can POST to /api/collect directly with the X-Produl-Server-Key header:
curl -X POST https://app.produl.tech/api/collect \
-H "Content-Type: application/json" \
-H "X-Produl-Server-Key: $PRODUL_SERVER_KEY" \
-d '{
"t": "ev",
"n": "invoice_paid",
"p": "/webhooks/stripe",
"u": "https://api.example.com/webhooks/stripe",
"vi": "cus_abc123",
"sk": "cus_abc123-session",
"pr": { "amount_cents": 4900 }
}'See API Reference for the full payload spec.