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.
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
admin-role key in a mobile app.
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).
https://capi-worker.topadroi.com/v1/mobile/eventsAccepts 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.
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.
| Field | Required | Notes |
|---|---|---|
site_id | yes | Must belong to your tenant. |
event_name | yes | e.g. Purchase, AddToCart, CompleteRegistration. Normalized per platform. |
event_id | yes | Unique UUID; the deduplication key. Persist it on the client so retries reuse the same ID. |
event_time | yes | Unix seconds. Clamped server-side if implausibly old/future. |
user_data | no | Identity & device signals — see below. |
event_data | no | value, currency, content_ids, contents, order_id, num_items. |
app_metadata | no | Device/app context — see below. |
consent | no | Tracking consent — see below. |
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.
| Field | Meaning |
|---|---|
em | Email (lowercased + hashed server-side). |
ph | Phone, E.164 (hashed server-side). |
external_id | Your own user/CRM id. |
idfa | iOS Advertising ID (only present when ATT authorized). |
gaid | Android Advertising ID. |
idfv | iOS Identifier for Vendor. |
app_user_id | App-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.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.
{
"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_reason | Meaning / action |
|---|---|
duplicate_within_window | Same event already seen — do not resend. |
quota_exceeded | Monthly event quota reached. Upgrade plan. |
rate_limited | Back off and retry later. |
site_not_owned / site_not_found | Check site_id and that the key's tenant owns it. |
invalid_event_shape | Missing event_name / event_id / event_time. |
internal_error | Transient server error — safe to retry with the same event_id. |
| Max events per batch | 100 |
| Max request body | 256 KB |
| Rate limits | Per-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.
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'});
idfa is only available after the user authorizes tracking. Set consent.ad_tracking from the ATT status; we map it to advertiser_tracking_enabled.gaid when the user has opted out.