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:
- When the user takes an action (clicks "Buy"), read
anonymousIdandsessionIdfrom the SDK - Include both in your request body to your own server
- 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
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
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
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:
// 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:
- Persist
anonymousIdwhen you first receive it (at checkout time). Store it in your database against the user or subscription. - Generate a new
sessionIdfor async events (crypto.randomUUID()). These events don't belong to any real client session - a fresh UUID is the correct value.
// 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:
| Step | Origin |
|---|---|
signed_up | Client |
app_created | Client |
first_event_verified | Client |
funnel_created | Client |
checkout_started | Client |
trial_started | Server (webhook) |
subscription_activated | Server (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.
