Developers
First event in under an hour. Schema enforced from day one.
Typed SDKs for every surface, a REST API for everything the dashboard can do, signed webhooks, and a tracking plan and journeys that live in your repository and ship through CI.
- checkout_started10:42:07u_5f2aiosvalid
- booking_confirmed10:42:05u_31c8androidvalid
- refund_issued10:42:03srv_billingservervalid
- session_started10:42:01u_88e3webvalid
- product_added10:41:59u_5f2aiosquarantined
- payment_failed10:41:57u_4a10iosvalid
{
"event": "checkout_started",
"user_id": "u_5f2a",
"platform": "ios",
"received": "10:42:07",
"properties": {
"cart_total": 2490,
"items": 3,
"currency": "INR"
}
}Quickstart
Install, identify, track.
Every SDK shares one core: an event queue that survives restarts, consent gating, identity that stitches anonymous history to a signed-in user, and the same wire format the server API accepts.
One package, one init. Events queue in the browser, flush in batches and survive a reload.
npm install @edverix/analytics-webimport { Analytics } from "@edverix/analytics-web";
const analytics = new Analytics({
apiKey: "edverix_live_XXXXXXXXXXXXXXXX",
endpoint: "https://api.edverix.com",
automaticTracking: true, // page views, sessions, flush on hide
});
await analytics.init();
// Called once the person signs in; earlier anonymous events are stitched.
analytics.identify("u_5f2a", { plan: "growth", city: "Pune" });
analytics.track("product_viewed", { sku: "DR-1044", price: 2490 });
// Ecommerce helper: standard events land in the revenue reports as-is.
analytics.ecommerce.orderCompleted({
orderId: "ord_88e3",
revenue: 1290,
currency: "INR",
products: [{ sku: "DR-1044", quantity: 1, price: 1290 }],
});Route through a hostname you own (a CNAME to api.edverix.com) to keep the SDK first-party.
SDK matrix
The same capabilities on every platform.
Client SDKs carry the queue, consent and lifecycle logic. The server API is the same endpoint the SDKs call, with nothing hidden behind them.
| Platform | Offline queue and dedupe | Consent gating | Automatic lifecycle events | Push token registration | In-app rendering | App inbox | Uninstall detection | First-party domain |
|---|---|---|---|---|---|---|---|---|
| Web@edverix/analytics-web | Yes | Yes | Sessions, page views | Web push | Yes | Yes | No | Yes |
| React@edverix/analytics-react | Yes | Yes | Sessions, page views | Web push | Yes | Yes | No | Yes |
| React Native@edverix/analytics-react-native | Yes | Yes | Yes | FCM, APNs | Yes | Yes | Yes | Yes |
| iOSEdverix (Swift package) | Yes | Yes | Yes | APNs | Yes | Yes | Yes | Yes |
| Androidcom.edverix:analytics-android | Yes | Yes | Yes | FCM | Yes | Yes | Yes | Yes |
| Flutteredverix_analytics | Yes | Yes | Yes | FCM, APNs | Yes | Yes | Yes | Yes |
| Server (HTTP)POST /v1/events | Dedupe on event_id | Your consent store | No | POST /v1/devices | No | No | POST /v1/devices/uninstalled | Any hostname |
First-party domain: events are sent to a hostname you own. Uninstall detection uses a silent push through your own Firebase or APNs credentials.
REST API
Everything the dashboard does, you can do over HTTPS.
One base URL, JSON in and out, keyset pagination, and the same tenant scoping on every call: an organisation cannot name another organisation's data, and a request for it is refused rather than filtered.
https://api.edverix.com
- POST
/v1/eventsIngest a batch of events; deduplicated on event_id.
- POST
/v1/devicesRegister a push token against a profile.
- POST
/v1/devices/uninstalledReport an uninstall from your own push pipeline.
- GET
/v1/eventsQuery events with property filters and keyset paging.
- GET
/v1/event-users/:id/timelineOne person's events, sessions and messages in order.
- POST
/v1/segmentsCreate a segment from a filter tree or a SQL query.
- POST
/v1/segments/:id/evaluateMembership for one user, or a count with a breakdown.
- POST
/v1/funnels/queryRun a funnel over any window with a breakdown property.
- POST
/v1/journeys/:id/simulateDry-run a journey against history; nothing is sent.
- POST
/v1/messages/transactionalSend a templated message to one person, outside journeys.
- POST
/v1/exportsExport events or profiles as JSON, CSV or Parquet.
curl -G "https://api.edverix.com/v1/events" \
-H "Authorization: Bearer edverix_live_XXXXXXXXXXXXXXXX" \
-H "x-project-id: prj_k3p9qw2ax" \
--data-urlencode "event_name=checkout_started" \
--data-urlencode "from=2026-09-01T00:00:00Z" \
--data-urlencode "filters@filters.json"[
{ "key": "cart.total", "operator": "gte", "value": 500 },
{ "key": "coupon", "operator": "not_exists" },
{ "key": "platform", "operator": "in", "value": ["ios", "android"] }
]Fourteen operators, each bound as a parameter. Dotted keys address nested properties.
- eq
- ne
- gt
- gte
- lt
- lte
- contains
- not_contains
- starts_with
- ends_with
- exists
- not_exists
- in
- not_in
Authentication
- SDK keys
- Sent as
X-API-Key. Write-only: they can post events and devices and nothing else, so they are safe to embed in a client app. Shown once at creation and stored as a peppered hash. - Service accounts
- Sent as
Authorization: Bearerfor query, management and export calls. Each account holds a primary and a secondary key so rotation never has a gap, and scopes limit what it may touch. - Project header
x-project-idselects the project inside your organisation. SDK keys are bound to one project already; service accounts name it on every call.
Webhooks
Signed, retried, circuit-broken.
Subscribe an HTTPS endpoint to the events you care about. Each delivery is signed with your secret, retried on failure, and paused rather than hammered when your endpoint is down.
What is sent
Pick any subset per endpoint. Message events carry the provider response; journey and segment events carry the person and the reason.
- message.sent
- message.delivered
- message.failed
- message.opened
- message.clicked
- message.bounced
- journey.entered
- journey.exited
- segment.entered
- segment.exited
- device.uninstalled
- profile.merged
- export.completed
- plan.violation
POST /hooks/edverix HTTP/1.1
Host: app.example.com
Content-Type: application/json
X-Edverix-Event: message.delivered
X-Edverix-Delivery: whd_01J8Q4M3ZK7N2R9V6T1B
X-Edverix-Timestamp: 1790327647
X-Edverix-Signature: v1=7f3c2a1b9e4d8c6f0a5b3e2d1c9f8a7b6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b
{
"id": "whd_01J8Q4M3ZK7N2R9V6T1B",
"type": "message.delivered",
"occurred_at": "2026-09-25T09:14:07Z",
"project_id": "prj_k3p9qw2ax",
"data": {
"message_id": "msg_31c8",
"channel": "whatsapp",
"user_id": "u_5f2a",
"journey_key": "cart_recovery",
"template": "cart_offer_10",
"provider": "meta_cloud_api"
}
}import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyEdverix(rawBody: string, headers: Headers, secret: string): boolean {
const ts = headers.get("x-edverix-timestamp") ?? "";
const given = (headers.get("x-edverix-signature") ?? "").replace(/^v1=/, "");
// Reject anything older than five minutes to stop replays.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(given, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Signed
Every delivery carries X-Edverix-Signature: HMAC-SHA256 over the timestamp and raw body with your endpoint's secret. Rotate the secret from the dashboard; the previous secret keeps verifying for 24 hours.
Retried
A non-2xx response or a 10-second timeout schedules a retry with exponential backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, then daily for 3 days. Delivery is at-least-once; deduplicate on the delivery id.
Circuit-broken
After 20 consecutive failures the endpoint is paused and project owners are notified. Deliveries queue for 7 days. Re-enable the endpoint, or fix it and let the health check re-enable it, and the queue replays in order.
Tracking plan as code
The schema lives in your repository and runs in CI.
Declare every event and property once. The CLI checks production traffic against the plan, generates types for the SDK, and quarantines events that break the contract instead of letting them poison a report.
# tracking-plan.yaml
version: 1
project: prj_k3p9qw2ax
events:
checkout_started:
description: A person opens the payment step with a non-empty cart.
owner: growth-team
sources: [web, ios, android]
properties:
cart_total: { type: number, required: true, min: 0 }
currency: { type: string, required: true, enum: [INR, USD, SGD] }
item_count: { type: integer, required: true }
coupon: { type: string, required: false, pattern: "^[A-Z0-9]{4,12}$" }
unknown_properties: quarantine
order_completed:
description: Payment captured and the order id issued.
properties:
order_id: { type: string, required: true }
revenue: { type: number, required: true, min: 0 }
currency: { type: string, required: true, enum: [INR, USD, SGD] }$ edverix plan check --plan tracking-plan.yaml --against production --since 7d
checkout_started
x cart_total sent as string "2490" in 1,204 events web 2.3.1
x currency value "inr" not in enum [INR, USD, SGD] ios 4.12.0
2 violations found · 1 quarantined (currency) · 1 coerced (cart_total)
Quarantined events are held outside reports and journeys.
Fix the source, then: edverix plan replay checkout_started --since 7d// Generated by `edverix plan types`. Do not edit.
export interface CheckoutStarted {
cart_total: number;
currency: "INR" | "USD" | "SGD";
item_count: number;
coupon?: string;
}
export interface OrderCompleted {
order_id: string;
revenue: number;
currency: "INR" | "USD" | "SGD";
}
export type TrackedEvents = {
checkout_started: CheckoutStarted;
order_completed: OrderCompleted;
};
declare module "@edverix/analytics-web" {
interface Analytics {
track<E extends keyof TrackedEvents>(name: E, properties: TrackedEvents[E]): void;
}
}Quarantine and replay
An event that violates its definition is stored in full but held outside reports, segments and journeys, so a malformed release cannot trigger a campaign or move a funnel. Coercible violations, such as a number sent as a string, are corrected on the way in and flagged for the owner. When the source is fixed, one command replays the quarantined window through the plan and everything that now validates takes its place in history at its original timestamp.
Journeys as code
Define a journey in YAML. Promote it like a deploy.
The canvas and the file are two views of the same definition. Diff a change, simulate it against real history, collect the approval the governance rules require, then promote. Nothing is sent until every gate passes.
# journeys/cart-recovery.yaml
key: cart_recovery
name: Cart recovery
trigger:
event: checkout_started
where: { cart_total: { gte: 500 } }
exit_on: [order_completed]
once_per: 7d
governance:
frequency_cap: default
quiet_hours: user_timezone
blast_radius: 5000/day
holdout: universal
steps:
- wait: 45m
- send: { channel: push, template: cart_reminder_v3 }
- wait: 6h
- branch:
on: opened(cart_reminder_v3)
yes:
- send: { channel: whatsapp, template: cart_offer_10 }
no:
- send: { channel: email, template: cart_reminder_email }
- wait: 24h
- exit$ edverix journeys diff cart_recovery --env production
~ steps[1].send.template cart_reminder_v2 -> cart_reminder_v3
+ steps[3].branch on opened(cart_reminder_v3)
~ governance.blast_radius 2500/day -> 5000/day
$ edverix journeys promote cart_recovery --to production
Simulating against the last 30 days of production events
would enter 4,318 people · 212 stopped by cap · 96 deferred by quiet hours · 43 held out
governance.blast_radius changed: approval required (journeys:approve)
Promotion queued · prm_01J8Q4M3ZK7N2R9V6T1B · nothing is sent until approvedThe simulation summary is the same one the dashboard shows before a journey goes live. A blast-radius change always requests approval from a role that holds the journeys:approve permission.
How journeys, simulation and approval fit togetherData model
Two kinds of people, one tenant boundary.
Your organisation owns projects, keys and the people who sign in to the dashboard. Your apps write end-users and events into a separate store that is scoped to the project on every row.
Dashboard users and tracked end-users are different tables. An end-user never authenticates against Edverix, their identifiers are chosen by you, and the same email can be two unrelated people under two organisations.
FAQ
Questions engineers ask first.
Does the web SDK work with server-side rendering?
Yes. The SDK touches window and document only inside init and the page helper, so importing it in a Next.js, Remix or TanStack Start module is safe. Call init on the client, typically in a provider effect; calls made before init resolves are buffered and replayed with their original timestamps.
What about ad blockers and tracking prevention?
Point the endpoint option at a hostname you own that forwards to api.edverix.com. The SDK then makes first-party requests with no third-party cookies, which is how it stays outside the lists blockers ship. Mobile SDKs are unaffected.
How are events batched and what happens offline?
Events queue in local storage on web and in AsyncStorage on React Native, flush every five seconds or at ten events by default, and flush again when the page hides or the app backgrounds. A failed flush keeps the batch and retries on the next cycle. Nothing is lost on a reload or a cold start.
Is ingestion idempotent?
Yes. Every event carries an event_id generated by the SDK, or supplied by you on the server API. Batches are deduplicated on that id per project, so at-least-once delivery and client retries never double count a conversion or fire a journey twice.
What do you do with personal data inside event properties?
Properties are sanitised at ingestion: values that look like passwords, card numbers or tokens are replaced with a redaction marker before anything is stored. Fields you mark as PII in the tracking plan are stored masked and unmasked only for a specific permission. Erasure requests remove a person end to end.
Can we self-host the platform?
Yes, on the Enterprise plan. The same dashboard, API, SDKs and worker run in your own cloud account and region, against a Postgres you control. Upgrades ship as versioned images and support covers the deployment as it would the hosted service.
Get started
Send your first event today.
Send your first event in an hour, simulate your first journey against real history, and keep every byte of it in a database you can query.
- No credit card for the Developer plan
- Bring your own providers
- Export everything, any time