⚠️ Unverified scaffold — not yet built or tested on a Flutter toolchain. The Dart, Kotlin, and Swift sources in this package were written to be clean, correct, and idiomatic, but have not been compiled or run againstflutter,gradle, orpod. Verify on a real machine before publishing.
A first-party mobile attribution SDK that connects an ad click → app install →
in-app conversions, reusing Decisa's existing public attribution ingest. It is
a "native pixel": it authenticates with your public app_key, reads the
platform's deferred-attribution signal on first launch, and then posts
identify/track events to Decisa's public ingest. Pixel membership is configured
server-side in the dashboard — no rebuild when you add pixels.
- Android is deterministic: the store redirect carries a match token
(
dcs_mclid) in the Play Install Referrer. - iOS is probabilistic: there is no referrer, so the install is matched server-side by IP + timestamp; the AdServices token enriches Apple Search Ads.
The SDK authenticates only with your app's public app_key — the
string that begins with dcs_app_. It is a public credential (same trust
class as the web pixel_key) and is sent in the request body. It is not a
secret and is safe to ship inside an APK/IPA.
Which pixels receive events is configured in the Decisa dashboard — add or remove member pixels without rebuilding the app or waiting for store review.
Never put a secret key in a mobile app. The server-side Decisa SDKs (Node,
PHP, Python) authenticate with a secret dcs_ak_ / dcs_sk_ key. A mobile
binary can be decompiled, so a secret in the binary can be extracted and used to
forge conversions and poison your attribution. This SDK refuses anything that is
not a dcs_app_ key (an assertion fires in debug builds).
Add the dependency (from pub.dev once published, or via a path/git ref while it lives in this monorepo):
dependencies:
decisa_sdk: ^0.2.0Minimum platform versions: Android minSdk 21, iOS 12.0 (the
AdServices token requires iOS 14.3+, guarded at runtime).
import 'package:decisa_sdk/decisa_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// First launch: reads the native deferred-attribution signal, POSTs
// /v1/resolve, and persists the visitor id + UTM attribution. Subsequent
// launches reuse the persisted visitor id (no re-resolve).
Decisa.start(appKey: 'dcs_app_your_public_key');
runApp(const MyApp());
}
// When a user signs in or is known, associate them. Email/phone are SHA-256
// hashed on-device — raw PII never leaves the phone.
await Decisa.identify(
userId: 'user_123', // sent as external_id (not hashed)
email: 'jane@example.com', // hashed client-side → email_sha256
);
// Record a conversion. The install's utm_* attribution (and the device madid,
// if already available) ride along in the event metadata.
await Decisa.track(DecisaEvent.purchase(value: 49.90, currency: 'USD'));DecisaEvent has a named constructor per canonical event, each mapped to the
right event_name the backend expects:
DecisaEvent.purchase(value: 49.90, currency: 'USD');
DecisaEvent.lead();
DecisaEvent.completeRegistration();
DecisaEvent.startTrial();
DecisaEvent.subscribe();
DecisaEvent.addToCart(value: 19.99, currency: 'USD');
DecisaEvent.initiateCheckout();
DecisaEvent.addPaymentInfo();
DecisaEvent.viewContent();
DecisaEvent.pageView();
DecisaEvent.search();
DecisaEvent.appInstall();
DecisaEvent.custom('viewed_pricing', metadata: {'plan': 'pro'});A fresh event_id (evt_ + UUID v4) is generated per event so retries dedupe
server-side and never double-count. The canonical names are
PageView, ViewContent, Search, AddToCart, AddPaymentInfo, InitiateCheckout, Lead, CompleteRegistration, Purchase, StartTrial, Subscribe, AppInstall, Custom.
The hard part of mobile attribution is connecting "who clicked the ad" to "who opened the app after installing" across a multi-day gap with no shared cookie. Decisa solves this with a server-minted click and a per-platform signal:
- Mint the click. Point your ad at a Decisa UTM short link in
?app=1mode:https://api.decisa.ai/k/<slug>?app=1. The backend mints a click (carrying the UTM attribution, a hashed IP, and a timestamp) and redirects the user to the right store. - Configure store URLs. On the UTM link's metadata set
android_store_urlandios_store_urlso the?app=1redirect sends each platform to the correct store listing. For Android, the Play redirect embedsdcs_mclid=<token>in the install referrer. - Resolve on first launch.
Decisa.initializereads the native signal:- Android — the Play Install Referrer's
dcs_mclid→ a deterministic match. - iOS — no referrer; the first
/v1/resolvecall is matched probabilistically by IP + timestamp server-side. The AdServices token additionally enriches Apple Search Ads campaigns only.
- Android — the Play Install Referrer's
- Bind and track.
/v1/resolvereturns avisitor_idbound to the click. The SDK persists it and uses it for every lateridentify/track— from then on it is just a native pixel reusing Decisa's existing matcher and CAPI fanout.
If resolve finds no match (or the key is unknown, returning a silent 204), the
SDK mints a local fallback visitor_id (v_…) so tracking still works; that
install is simply unattributed.
The SDK does not prompt for App Tracking Transparency (ATT) or read the IDFA in v1.
madid(IDFA/GAID) is attached to event metadata only if it is already available without a prompt.
All endpoints are public and live under the base URL (default
https://api.decisa.ai). Responses use the envelope { data, meta, error }.
| Endpoint | Purpose | Returns |
|---|---|---|
POST /v1/resolve |
First-run deferred-attribution lookup. Body: { app_key, mclid?, adservices_token? }. |
200 { data: { visitor_id, matched, match_type, utm_* } } or silent 204 for an unknown key. |
POST /v1/identify |
Associate hashed identity with the visitor. Body: { app_key, visitor_id, email_sha256?, phone_sha256?, fn_sha256?, ln_sha256?, external_id? }. |
202 |
POST /v1/track |
Record a pixel event. Body: { event_id, event_name, visitor_id, app_key, value? | value_cents?, currency?, url?, occurred_at?, is_test?, metadata? }. |
202 |
lib/
decisa_sdk.dart # public exports (Decisa, DecisaEvent, DecisaAttribution)
src/
decisa_client.dart # Decisa.initialize / identify / track orchestration
decisa_event.dart # DecisaEvent + named constructors + wire mapping
decisa_attribution.dart # resolved attribution model + JSON (de)serialization
transport.dart # HTTP POST + { data, meta, error } envelope decode
persistence.dart # shared_preferences-backed visitor_id / external_id
hashing.dart # client-side SHA-256 of email/phone/name
deferred_channel.dart # MethodChannel('ai.decisa.sdk/deferred') wrapper
android/ # Kotlin plugin: Play Install Referrer → dcs_mclid
ios/ # Swift plugin: AdServices attribution token
example/ # minimal example app
test/ # Dart unit tests (injectable transport/persistence/channel)
The transport, persistence, and deferred-signal channel are all injectable on
Decisa.initialize (@visibleForTesting), which is how the unit tests exercise
resolve/identify/track without a live backend or a real device.
MIT. See LICENSE.