Events from your server
Some events happen where the SDK cannot see them: a payment your backend confirms, a renewal the App Store charges at 3am. Send them from your server with a secret key, and they land on the same person, the same device and — when it was just now — the same session as everything the app sent.
Why from your server
Your write key is public. It ships inside your app and your web pages, so anybody can use it to send purchase with any amount. For most events that does not matter. For money it does, and the thing that knows a payment really happened is your backend, or the store that charged it.
So a sale should be reported by whatever confirmed it, with a secret the app never holds. The app still sends everything that led up to it — the product views, the cart, the checkout steps — and one short token ties the two together.
How it fits together
1Make a server key
In the dashboard, Settings → API keys → Send server events, choosing the environment its events land in. It starts with sk_, lives only on your server, and every event it sends goes to that environment, whatever the body says.
2Ask the SDK for a context token, just before the purchase
contextToken() returns a short id that stands for this person, this device and this session. Send it to your backend with the checkout request, or hand it to the store.
3Send the event with the token
Your backend confirms the payment and posts the event with the token in context.treebars. Treebars files it where the token points.
In the app
Once per purchase, right before it — not on every request:
// Web
const token = await treebars.contextToken();
// React Native
const token = await Treebars.contextToken();
// iOS
let token = await Treebars.contextToken() // a UUID
// Android
val token = Treebars.contextToken() // from a coroutine
// Then send it with the request that will lead to the event:
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ cart, treebarsContext: token }),
});null when Treebars could not be reached in a few seconds, before the SDK has started, or while the person has opted out. Go ahead with the purchase anyway: the sale is still reported, it only loses the device and session it would have been filed under.On your backend
The body is Segment's track call. userId is the account the purchase belongs to when there is one; for a guest, leave it out and the token names them.
curl -X POST https://api.treebars.com/v1/track \
-H 'Authorization: Bearer sk_…' \
-H 'Content-Type: application/json' \
-d '{
"userId": "user_42",
"event": "purchase",
"messageId": "order_1234",
"properties": { "value": 49.90, "currency": "EUR", "order_id": "order_1234" },
"context": { "treebars": "3f2a9c1e-7b4d-4e8a-9c21-5d6e7f809a1b" }
}'await fetch('https://api.treebars.com/v1/track', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.TREEBARS_SERVER_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: user?.id, // omit for a guest
event: 'purchase',
messageId: order.id, // the same id twice is one event
properties: { value: order.total, currency: order.currency, order_id: order.id },
context: { treebars: req.body.treebarsContext },
}),
});requests.post(
"https://api.treebars.com/v1/track",
headers={"Authorization": f"Bearer {os.environ['TREEBARS_SERVER_KEY']}"},
json={
"userId": user.id,
"event": "purchase",
"messageId": order.id,
"properties": {"value": order.total, "currency": order.currency, "order_id": order.id},
"context": {"treebars": body["treebarsContext"]},
},
)Already on Segment or RudderStack? Point their server library at https://api.treebars.com with the server key as its write key, and pass the token in the call's context.
From a store or a payment provider
Connect the provider under Data & tools → Integrations, then hand it the same token at purchase. Every renewal and refund it sends later carries it too.
// App Store (StoreKit 2)
let token = await Treebars.contextToken()
let result = try await product.purchase(options: token.map { [.appAccountToken($0)] } ?? [])
// Google Play Billing (from a coroutine)
val token = Treebars.contextToken()
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(products)
.apply { token?.let(::setObfuscatedAccountId) }
.build()
// Stripe Checkout (on your backend, with the token the page sent)
await stripe.checkout.sessions.create({
client_reference_id: user.id,
metadata: { treebars_user_id: user.id, treebars_context: token },
subscription_data: { metadata: { treebars_user_id: user.id, treebars_context: token } },
// …
});
// RevenueCat
Purchases.shared.attribution.setAttributes(["treebars_context": token.uuidString.lowercased()])What the token links
Filed where
- The userId you send. With none, whoever asked for the token — a guest too, and when that guest signs in later, the account they became.
- The phone or browser that asked, with its platform, OS and app version. So an iPhone purchase is an iPhone’s, and counts toward the ad that brought that install.
- Only when the event happens within 30 minutes of the token. The purchase joins the checkout’s session; a renewal next month, carrying the same token, does not — nobody was in the app.
Renewals, refunds and anything later
Events your backend sends long after the purchase — a renewal it charges itself, a refund, a shipment — send with userId. They are filed under the person, outside any session. Keep the purchase's token beside the order or the subscription and send it again if you want them on the same device too; it joins a session only within half an hour of the token, so a renewal a month later never does. For a guest with no account, the stored token is how you name them.
Give every event about one purchase the same order_id or subscription_id, so a report can follow a subscription from the session it started in to every renewal after it.
Count money only from your server
In Settings → Revenue, each money event has a Server only switch. Turn it on for a sale your server or a store reports, and a purchase sent with the public write key stays an event but no longer counts as money — on any chart, or in the value sent to your ad networks. It is off until you turn it on.
purchase and your server sends it too, both count — unless the server-only switch is on, or the app sends something like checkout_completed instead.What the answer says
200 with how many were accepted. A warnings list names any event that was kept but not linked, and why — send it again only for errors, never for a warning, or you will file it twice. A time more than 30 days back is refused: older history is imported, not sent.
When the token links nothing
- A test build’s token sent with a live key, or the other way round. Use the key for the environment the app reports to.
- The phone that asked belongs to someone else than the userId you sent. The event is kept under your userId, on no device.
- Send exactly what contextToken() returned. A token-only event with a bad token names nobody and is refused.
Testing it
Make a second server key for your test environment and point your staging backend at it, with a test build of the app. Treebars' own demo shop does all of this — a checkout, a declined payment, a subscription and its renewal — in samples/web/server.