Developer Reference · Mobile Events API & SDK ← Home

Mobile Events API & SDK

Send conversion events from your iOS, Android, React Native or Flutter app to topadroi. We normalize, deduplicate, and fan them out server-side to 11 ad platforms' Conversions APIs (Meta, TikTok, Google, Snap, Pinterest, Reddit, and more) — so you ship one lightweight SDK instead of integrating each platform.

Positioning. topadroi is a server-side signal forwarder, the mobile counterpart of our server-side GTM — not a mobile measurement partner (MMP). We don't do attribution or SKAdNetwork conversion-value management; we forward your validated conversions to every ad platform with correct app-event formatting. Run us alongside your MMP.
Contents

Authentication

All requests use a tenant ingest API key as a Bearer token. Create a dedicated mobile ingest key in your tenant portal so it can be rotated independently of your web key.

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx
The ingest key is embedded in your app binary and can be extracted by reverse engineering — this is expected. Security does not rely on key secrecy: every request is bound to sites your tenant owns, and per-IP / per-tenant rate limits plus anomaly alerts contain abuse. Never put an admin-role key in a mobile app.

Optional: bundle-id allowlist (X-App-Id)

Our SDKs send an X-App-Id header containing the app's bundle id (iOS) / package name (Android). If you configure an allowlist for a site, requests whose X-App-Id is not on the list are rejected (skip_reason: bundle_not_allowed). Leaving it empty disables the check.

Honest limitation: X-App-Id is a self-reported, forgeable header — an attacker who reverse-engineers your app obtains both the key and the bundle id. This is defense-in-depth (it raises the bar when a key leaks outside the binary), not strong app attestation. For real device/app integrity, use Apple App Attest / Play Integrity (not yet integrated).

Endpoint

POST  https://capi-worker.topadroi.com/v1/mobile/events

Accepts a batch of events (recommended) or a single event object. Events are validated, deduplicated, metered against your quota, archived for retry, and dispatched to every enabled destination for the target site.

Request body

Batch form (preferred — buffer events client-side and flush periodically):

{
  "events": [
    {
      "site_id": "site_abc",
      "event_name": "Purchase",
      "event_id": "1d9c1f9e-7c1a-4c2e-9b3a-9f0c2b6d4e11",
      "event_time": 1747900000,
      "user_data": {
        "em": "user@example.com",
        "ph": "+15555550123",
        "external_id": "crm_12345",
        "idfa": "AEBE52E7-03EE-455A-B3C4-E57283966239",
        "gaid": "38400000-8cf0-11bd-b23e-10b96e40000d",
        "idfv": "FBEC4A29-3D8F-4E1B-9A0E-1234567890AB"
      },
      "event_data": { "value": 89.97, "currency": "USD", "content_ids": ["SKU-1"] },
      "app_metadata": {
        "platform": "ios",
        "app_version": "3.2.1",
        "os_version": "18.4",
        "device_model": "iPhone16,2",
        "locale": "en-US",
        "timezone": "America/Los_Angeles"
      },
      "consent": { "ad_tracking": true, "analytics": true }
    }
  ]
}

A single event object (without the events wrapper) is also accepted.

Top-level fields

FieldRequiredNotes
site_idyesMust belong to your tenant.
event_nameyese.g. Purchase, AddToCart, CompleteRegistration. Normalized per platform.
event_idyesUnique UUID; the deduplication key. Persist it on the client so retries reuse the same ID.
event_timeyesUnix seconds. Clamped server-side if implausibly old/future.
user_datanoIdentity & device signals — see below.
event_datanovalue, currency, content_ids, contents, order_id, num_items.
app_metadatanoDevice/app context — see below.
consentnoTracking consent — see below.

user_data fields

Send what you have; more signals = higher match quality. Email/phone are normalized and SHA-256 hashed server-side (send them in the clear over TLS — do not pre-hash). Device IDs are routed to each platform in the format that platform requires (raw vs hashed) automatically.

FieldMeaning
emEmail (lowercased + hashed server-side).
phPhone, E.164 (hashed server-side).
external_idYour own user/CRM id.
idfaiOS Advertising ID (only present when ATT authorized).
gaidAndroid Advertising ID.
idfviOS Identifier for Vendor.
app_user_idApp-scoped user id (high-value first-party signal).
Also accepted: fn, ln, ct, st, zp, country, ge, db, fbp, fbc, gclid, ttclid, anon_id, fb_login_id, subscription_id, lead_id.
idfa (iOS) or gaid (Android) becomes the device advertiser id we forward. We never persist the raw device id — the retry archive stores only hashed identifiers.

app_metadata & consent

app_metadata.platform (ios | android) is important: it routes the device id to each platform's correct iOS/Android field. The rest builds Meta's extinfo and similar app context.

consent.ad_tracking drives advertiser_tracking_enabled on platforms that require it. On iOS, set it from the ATT authorization status. If consent is absent or denied, we send first-party hashed signals only and omit the device id.

Response

{
  "ok": true,
  "accepted": 2,
  "total": 2,
  "results": [
    { "event_id": "…", "accepted": true },
    { "event_id": "…", "accepted": false, "skip_reason": "duplicate_within_window" }
  ]
}

Each event is reported independently. Common skip_reason values:

skip_reasonMeaning / action
duplicate_within_windowSame event already seen — do not resend.
quota_exceededMonthly event quota reached. Upgrade plan.
rate_limitedBack off and retry later.
site_not_owned / site_not_foundCheck site_id and that the key's tenant owns it.
invalid_event_shapeMissing event_name / event_id / event_time.
internal_errorTransient server error — safe to retry with the same event_id.

Limits & test mode

Max events per batch100
Max request body256 KB
Rate limitsPer-IP and per-tenant (by plan); 429 with retry-after.

Test mode: append ?test=1 (or header X-Test-Event: 1) to run the full pipeline without consuming quota or affecting billing. The response includes "test_mode": true.

SDK quickstart

The same API across all four SDKs. Initialize once, then track events; the SDK buffers offline and flushes in batches. ⬇ Download the SDKs →

import TopadroiSDK

Topadroi.initialize(apiKey: "sk_live_…", site: "site_abc")
Topadroi.requestTrackingAuthorization()          // ATT prompt (you choose when)
Topadroi.setConsent(.init(adTracking: true, analytics: true))
Topadroi.identify(.init(email: "user@example.com"))
Topadroi.track("Purchase", ["value": 89.97, "currency": "USD", "content_ids": ["SKU-1"]])
import com.topadroi.sdk.Topadroi

Topadroi.initialize(context, apiKey = "sk_live_…", site = "site_abc")
Topadroi.setConsent(Consent(adTracking = true, analytics = true))
Topadroi.identify(Traits(email = "user@example.com"))
Topadroi.track("Purchase", mapOf("value" to 89.97, "currency" to "USD"))
import { Topadroi } from '@topadroi/react-native'

Topadroi.initialize({ apiKey: 'sk_live_…', site: 'site_abc' })
Topadroi.setConsent({ adTracking: true, analytics: true })
Topadroi.identify({ email: 'user@example.com' })
Topadroi.track('Purchase', { value: 89.97, currency: 'USD' })
import 'package:topadroi_sdk/topadroi_sdk.dart';

await Topadroi.initialize(apiKey: 'sk_live_…', site: 'site_abc');
await Topadroi.setConsent(adTracking: true, analytics: true);
await Topadroi.identify(email: 'user@example.com');
await Topadroi.track('Purchase', {'value': 89.97, 'currency': 'USD'});

Privacy & consent

Questions? Contact us →