API
Sending events
'POST /api/log: the endpoint the script and SDKs send to, and the one your server can call with an API token. The body, the headers and the answers.'
Every event reaches numbers through one endpoint. The script and the SDK call it for you; call it yourself to send events from a server.
POST https://numbers.obelism.studio/api/log
Content-Type: text/plain;charset=UTF-8One event or exposure per request, at most 8 KB. The body is JSON either way: the script and SDK send it as text/plain because that is CORS-safelisted, so the browser skips the preflight and an event costs one request. A JSON Content-Type also works; it costs a preflight, which is cached for a day.
Who may send
- A browser, from an origin allowed for the Space and environment (on the Space's page on www.obelism.studio, under Allowed origins).
developallowslocalhoston any port. - A server, with one of the environment's API tokens in a
tokenheader. The tokens are numbers' own: make one under API tokens on the setup page, for the Space and environment.
The body
type Common = {
organizationId: string
// The Space the event belongs to. Without it, the Organization's first.
spaceId?: string
environment: 'develop' | 'staging' | 'production'
visitorId: string // the obelism_visitor_id cookie: `visi_` and 26 characters
pointer: 'coarse' | 'fine'
device:
| 'wearable'
| 'mobile'
| 'tablet'
| 'console'
| 'smarttv'
| 'embedded'
| 'desktop'
browser:
| 'chrome'
| 'safari'
| 'firefox'
| 'edge'
| 'ie'
| 'samsung internet'
| 'social'
| 'other'
os: 'mac os' | 'ios' | 'android' | 'windows' | 'unix'
screen: 'small' | 'medium' | 'large' | 'huge' | '' // '' from a server, which doesn't know it
visitor: 'recurring' | 'new'
}
type Event = Common & {
type: 'event'
event: string // `page_view`, `click`, `sign_up`, …
scope: string // required, not empty
value?: number
currency?: string // at most 8 characters
params?: Record<string, unknown>
// The page it happened on (all optional).
path?: string // starts with "/"; the query string is dropped
referrer?: string // a URL; kept without its query string
utmSource?: string
utmMedium?: string
utmCampaign?: string
}
// A visitor's variant of an Improve test or flag, sent by the Improve SDK.
type Exposure = Common & {
type: 'exposure'
subjectKind: 'test' | 'flag'
subjectId: string
variant: string
}Strings are at most 256 characters, path 1024.
Example
curl https://numbers.obelism.studio/api/log \
-H 'Content-Type: application/json' \
-H 'token: <API token>' \
-d '{
"type": "event",
"event": "purchase",
"scope": "checkout",
"value": 59.95,
"currency": "EUR",
"organizationId": "org_…",
"spaceId": "space_…",
"environment": "production",
"visitorId": "visi_…",
"pointer": "fine",
"device": "desktop",
"browser": "other",
"os": "unix",
"screen": "large",
"visitor": "recurring"
}'Pass the visitor's obelism_visitor_id cookie along from the request, so the event counts for the same visitor as their page views. The old visitorId cookie is read too, for visitors from before the rename.
From a storefront
A React Router loader or action on Shopify Oxygen runs on the server, so it sends like any other server: the environment's API token in token, the visitor's obelism_visitor_id cookie, and no Origin header. The token belongs to one Space and environment, so name the same Space in spaceId.
The device fields come from the user agent: detectDevice() in @obelism/numbers-sdk gives pointer, device, browser and os. screen is a browser width bucket, and a server doesn't know it, so send ''.
import { detectDevice } from '@obelism/numbers-sdk'
const readCookie = (header: string | null, name: string) =>
header
?.split('; ')
.find((row) => row.split('=')[0] === name)
?.split('=')[1] ?? null
type Order = { id: string; total: number; currency: string }
export const sendOrder = async (request: Request, env: Env, order: Order) => {
const visitorId =
readCookie(request.headers.get('cookie'), 'obelism_visitor_id') ??
readCookie(request.headers.get('cookie'), 'visitorId')
if (!visitorId) return
await fetch('https://numbers.obelism.studio/api/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token: env.NUMBERS_API_TOKEN,
},
body: JSON.stringify({
type: 'event',
event: 'purchase',
scope: 'checkout',
organizationId: env.NUMBERS_ORGANIZATION_ID,
spaceId: env.NUMBERS_SPACE_ID,
environment: env.NUMBERS_ENVIRONMENT,
visitorId,
...detectDevice(request.headers.get('user-agent') ?? ''),
screen: '',
visitor: 'recurring',
value: order.total,
currency: order.currency,
params: { order_id: order.id },
path: '/checkout',
}),
})
}Send it once per order: from the order's orders/paid webhook, or from the confirmation route guarded by the order id, not on every render of it.
Two things a server request doesn't have. The visitor's IP: the rate limit of 300 a minute keys on it, and a request from Oxygen has none, so all server events share one bucket per Organization. Send one event per order, and retry after a 429's Retry-After. The country: numbers reads it from the request's edge headers, which a Worker's subrequest usually doesn't carry, so the event may have no country. Browser events still do.
The answer
200 with { "success": true } once the event is queued; it is stored within seconds. Anything else is an error in problem JSON. The Api-Version header is optional; this API is version 1.