---
title: "Native apps"
description: "React Native, Expo, Swift and Kotlin apps post straight to /ingest with a platform and app tag. No SDK, no web view, no device identifiers."
last_updated: "2026-08-21"
---

# Native apps

A native app has no `<script>` tag to load `lytics.js` into, so it talks to `/ingest` directly. The endpoint is a plain JSON POST with no key on it, which is the whole integration — there is no native SDK to install.

Two fields separate app traffic from website traffic in the same project:

- **`platform`** — `"ios"` or `"android"`. The browser snippet sends `"web"`.
- **`app`** — which client sent it, e.g. `"myapp-mobile"`. Lowercase letters, digits, `.`, `_` and `-`, up to 64 characters. Websites set the same tag with [`data-app`](/docs/tracking/script-reference/#attributes).

Both show up in the dashboard's **Devices & apps** card.

## Sending an event

```ts
await fetch("https://api.analytics.canarycoders.es/ingest", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    // Native requests carry no Origin of their own, and ingest checks it
    // against the project's trusted domains. Send the product's own origin.
    Origin: "https://myapp.com",
  },
  body: JSON.stringify({
    domain: "myapp.com",       // must be one of the project's trusted domains
    sessionId,                  // UUID v4, see below
    eventType: "page_view",     // or "custom" with an eventName
    path: "/hotspot/teide",     // the app's own route
    platform: "ios",
    app: "myapp-mobile",
    deviceType: "mobile",       // or "tablet" — the app knows, no UA to read it off
    locale: "en-GB",
  }),
});
```

Ingest answers `204` to everything, including rows it drops, so there is nothing to branch on. Fire and forget.

## Sessions

The browser snippet scopes a session to a tab (`sessionStorage`). An app has no tab, so mint a UUID v4, persist it, and rotate it after a stretch of inactivity — **30 minutes** is the usual choice and what CanaryPulse's own app uses. Sessions are how unique visitors are counted, so a single id that never rotates collapses the whole install into one visitor forever.

<Aside type="caution" title="Do not use a device id">
Reaching for an install id, IDFV or advertising id instead of a rotating session id breaks the privacy contract the rest of CanaryLytics is built on. Nothing in the pipeline needs one.
</Aside>

## What the server does differently for native events

- **No bot classification.** React Native's default Android user agent is a bare `okhttp/4.12.0`, which every bot list matches. Native events skip the check outright rather than being labelled crawlers and hidden from stats.
- **`os` comes from the platform tag** (`ios` → iOS, `android` → Android) since there is no browser UA to parse, and `browser` stays empty.
- **`deviceType` comes from the app**, falling back to `mobile` when it is not sent.

## Paths

Report the app's own routes. They will not match the website's URLs — `/hotspot/teide` in an app against `/en/hotspots/tenerife/h/teide` on the web — so **Top pages** lists both shapes side by side and the app tag is what tells them apart. Rewriting native routes into web URLs is rarely worth the mapping code.

## Related

<CardGrid>
  <LinkCard title="Capacitor & Ionic" description="Hybrid apps run a web view, so they load the snippet instead." href="/docs/environments/capacitor/" />
  <LinkCard title="Script reference" description="data-app and the rest of the browser snippet's config." href="/docs/tracking/script-reference/" />
</CardGrid>

## Sitemap

- [Docs sitemap](https://analytics.canarycoders.es/docs/sitemap.md)
- [Full documentation as a single file](https://analytics.canarycoders.es/llms-full.txt)
