← All docs

Server-side events

Send events from your backend with the secret key, and the ingest limits that apply.

Some events shouldn’t come from a browser. Payments that settle asynchronously, subscription renewals, background jobs, anything a user can’t be present for. Those go through the server ingest endpoint with your secret key.

Public key vs secret key

Public keySecret key
Formatlp_pub_ + 22 base58 charslp_sec_ + 43 base58 chars
PurposeBrowser SDK init()Server-side ingest
ExposureDesigned to ship in client code — safe and expectedNever in browser code. Read it from an environment variable
StorageStored plaintext, viewable any time in Settings → InstallOnly a hash is stored — shown exactly once, at project creation or key rotation

Because only a hash of the secret key is stored, LaunchPulse cannot show it to you again. If you lose it, rotate the key in Project settings — which invalidates the old one — and store the new value immediately.

Use the environment variable name LAUNCHPULSE_SECRET. That’s the name the app’s AI-agent prompt tells coding agents to use, so sticking to it means an agent wiring up your integration will find it.

The endpoint

POST https://collector.launchpulse.dev/v1/events
Authorization: Bearer lp_sec_...
Content-Type: application/json

One event per request.

curl -X POST https://collector.launchpulse.dev/v1/events \
  -H "Authorization: Bearer $LAUNCHPULSE_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "subscription_paid",
    "user_id": "user_8f21c",
    "properties": {
      "$revenue": 49.99,
      "$currency": "USD",
      "plan": "annual"
    }
  }'

Node, with fetch:

async function sendToLaunchPulse(event) {
  const res = await fetch("https://collector.launchpulse.dev/v1/events", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LAUNCHPULSE_SECRET}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(event),
  });

  if (!res.ok) {
    console.error("LaunchPulse ingest failed", res.status);
  }
}

await sendToLaunchPulse({
  event: "trial_converted",
  user_id: user.id,
  properties: { plan: "pro", seats: 3 },
});

Use the same user_id your browser code passes to identify(). Same id in both places means one actor. Different ids mean the same person counted twice.

Browser endpoint, for comparison

The SDK posts batches to a different endpoint with the public key in the body:

POST https://collector.launchpulse.dev/e
{ "k": "lp_pub_...", "events": [ ... ] }

You don’t normally call that yourself — that’s what the SDK does. Details in the SDK reference.

ingest_source

Events sent from the server are stamped ingest_source: 'server'. Browser events are 'browser'. You can filter on it in the events log, which is useful when you’re checking whether an integration is actually firing.

Limits

LimitValue
Request body size256 KB
Max events per request500 (else HTTP 413)
Rate limit6000/min per public key, 6000/min per IP (else HTTP 429)

Property limits are the same as the browser SDK — 64 properties, 64-character keys, 1024-character string values, depth 3, 32 entries per array or object, 8192 bytes serialized. Violating any one of them rejects the event. The full table is in the SDK reference.

$revenue and $currency work exactly as they do in the browser. Any other $-prefixed property key rejects the event. See Revenue tracking.

Two behaviors that will confuse you if you don’t know them

A 2xx does not mean your key works

Unknown, revoked and paused keys all receive a uniform 2xx response. The API deliberately does not reveal whether a key exists or what state it’s in — that’s an anti-enumeration measure, so nobody can probe your keys by watching status codes.

The consequence for you: a 200 does not prove your key is valid. If you’re wiring up an integration and want confirmation, use the test screen during onboarding, or check the events log. Data appearing in the log is the only proof.

Paused projects behave the same way — events are accepted and dropped.

Timestamps are clamped

If you send a client timestamp, it is clamped to the window [server receive time − 5 minutes, server receive time]. The server’s receive time is authoritative.

This means you cannot backfill history through this endpoint, and you cannot send events dated in the future. A queued job that retries an hour later will land at the retry time, not the original time. If exact timing matters, send events as they happen rather than batching them into a nightly run.

When to send server-side

  • Revenue. Payment settles on your backend, not on a thank-you page a user can reload. See Revenue tracking.
  • Renewals and churn. No browser involved at all.
  • Anything a user shouldn’t be able to fake. Browser events come from a client you don’t control.
  • Events from background jobs — an export finishing, an import completing, a scheduled report going out.

Keep pageviews and in-app interactions in the browser. The SDK already handles identity, sessions and attribution there, and reproducing that server-side is work with no payoff.

Keeping the key safe

  • Environment variable, never a committed file. LAUNCHPULSE_SECRET.
  • Never in a bundle, a client component, or anything with NEXT_PUBLIC_ on it.
  • Rotate it in Project settings if it ever lands somewhere public. Rotation invalidates the old key immediately.

More in Security.

NextAccount and billing

Talk to us

Questions about LaunchPulse, or want a walkthrough? Send a note and a real person replies.

Or email us at hello@launchpulse.dev