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

bash
npm install produl-server

Create a server key

  1. 1

    Open the site's Settings

    In your dashboard, navigate to the site you want to track and open Settings → Server keys.

  2. 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. 3

    Set the environment variable

    bash
    PRODUL_SERVER_KEY=sk_••••••••••••••••••••••••

Treat server keys like passwords

A server key can write events for the site it's associated with. Never commit one to source control, never embed it in client-side code, and rotate it if it leaks.

Usage

app/webhooks/stripe.tsts
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:

MethodSignatureNotes
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

FieldTypeRequiredDescription
pathstringyesNormalized path (e.g. /pricing)
visitorIdstringyesStable identifier for the visitor
urlstringnoFull URL, defaults to https://server{path}
titlestringnoPage title
referrerstringnoReferrer URL
sessionKeystringnoGroups 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.

ts
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:

bash
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.