Server-side events

Track events that happen on your backend - purchases, trial activations, subscription changes - and tie them to the same user journey captured by the client SDK.


When to use this

Client SDKs track what users do inside your app. Some milestones happen on your server:

  • A payment webhook fires after a successful purchase
  • A trial is provisioned when a user completes checkout
  • A subscription renews or cancels asynchronously

For these events to appear in your funnels, your server needs to send them to OnRamp. No additional package is required - it's a direct HTTP call to the same ingestion endpoint the client SDKs use.


How it works

The key constraint: every OnRamp event must carry the user's anonymous_id (and ideally their session_id). These IDs live on the client. Your server doesn't know them unless your client passes them.

The pattern:

  1. When the user takes an action (clicks "Buy"), read anonymousId and sessionId from the SDK
  2. Include both in your request body to your own server
  3. Your server fires the OnRamp event using those IDs

This keeps the server event tied to the exact session that triggered the action.


Step 1 - Read IDs from the client SDK

React / Next.js

tsx
import { useOnRamp } from '@onramp-sdk/react'

function CheckoutButton() {
  const { step, getIds } = useOnRamp()

  async function handleClick() {
    step('checkout_started')
    const { anonymousId, sessionId } = getIds()

    await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId: 'price_xxx',
        anonymousId,   // pass to your server
        sessionId,
      }),
    })
  }

  return <button onClick={handleClick}>Upgrade</button>
}

React Native

tsx
import { OnRamp } from '@onramp-sdk/react-native'

async function handlePurchase() {
  OnRamp.step('purchase_initiated')
  const { anonymousId, sessionId } = OnRamp.getIds()

  await fetch('https://api.yourapp.com/purchase', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ productId: 'pro_monthly', anonymousId, sessionId }),
  })
}

Don't cache the IDs

Call getIds() at the moment of the action. Session IDs rotate after 30 minutes of inactivity, so a stale ID stored from an earlier call may not match the user's current session.


Step 2 - Fire the event from your server

Your server receives anonymousId and sessionId in the request body and sends the event to OnRamp's ingestion endpoint.

Node.js / TypeScript

ts
const ONRAMP_INGEST = 'https://ingest.getonramp.dev'

async function trackOnRampEvent(params: {
  anonymousId: string
  sessionId: string
  stepName: string
  properties?: Record<string, string | number | boolean>
}) {
  await fetch(`${ONRAMP_INGEST}/v1/events`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-onramp-key': process.env.ONRAMP_API_KEY!,
    },
    body: JSON.stringify({
      events: [{
        schema_version: '1.0',
        event_id: crypto.randomUUID(),
        event_type: 'step_entered',
        app_key: process.env.ONRAMP_API_KEY!,
        anonymous_id: params.anonymousId,
        session_id: params.sessionId,
        step_name: params.stepName,
        step_index: 0,
        client_timestamp_ms: Date.now(),
        platform: 'other',
        os_version: null,
        app_version: null,
        device_model: null,
        device_type: null,
        properties: params.properties ?? null,
      }],
    }),
  })
}

Then call it in your checkout or webhook handler:

ts
// In a Stripe webhook, after checkout.session.completed
const anonymousId = checkout.metadata.anonymous_id
const sessionId   = checkout.metadata.session_id

if (anonymousId && sessionId) {
  await trackOnRampEvent({
    anonymousId,
    sessionId,
    stepName: 'trial_started',
    properties: { plan: 'pro', period: 'monthly' },
  })
}

Async webhook events

When an event happens asynchronously - a subscription renews, a trial converts days after checkout - you won't have a live sessionId. In this case:

  1. Persist anonymousId when you first receive it (at checkout time). Store it in your database against the user or subscription.
  2. Generate a new sessionId for async events (crypto.randomUUID()). These events don't belong to any real client session - a fresh UUID is the correct value.
ts
// Triggered days later when trial converts to paid
const user = await db.users.findBySubscriptionId(sub.id)

await trackOnRampEvent({
  anonymousId: user.onrampAnonymousId,  // stored at checkout
  sessionId: crypto.randomUUID(),        // async - no live session
  stepName: 'subscription_activated',
  properties: { plan: user.plan },
})

Using your API key server-side

Your OnRamp API key is the same one used in the client SDK. It is safe to use server-side - it authenticates requests to the ingestion endpoint and is scoped to your app only.

Store it as an environment variable (ONRAMP_API_KEY) and never expose it in client-side code beyond what the SDK already sends.


Defining funnels with server events

Server events appear in the dashboard alongside client events. To include them in a funnel, simply add the step name (trial_started, subscription_activated, etc.) to your funnel definition in the dashboard - they show up automatically.

A typical full-journey funnel mixing client and server events:

StepOrigin
signed_upClient
app_createdClient
first_event_verifiedClient
funnel_createdClient
checkout_startedClient
trial_startedServer (webhook)
subscription_activatedServer (webhook)

Privacy

When you store anonymous_id in your database and associate it with a real user account, you become the data controller linking that pseudonymous identifier to a person. OnRamp only sees the UUID - it never knows who the person is.

You should mention OnRamp as a sub-processor in your own privacy policy when using server-side tracking. See our Privacy & data page for what OnRamp stores and for how long.