Skip to content

Ads

Prove to your advertising partners that an impression or a click came from a real person. On Flutter, your app tells the SDK when an ad is on screen, clicked and gone; the SDK signs a proof for each with the device key and sends it to DEA's recording service, which keeps them.

The SDK does not request, render or wrap ads. Your ad stack keeps doing that.

What Flutter has

The Flutter SDK has the lower-level proof calls on DeaSdk: sendProof(), record() and getProof(). Each signs a proof for one ad event, ProofEvent.adImpression, adClick or adEnded, with the score the scoring engine has at that moment. Your app makes the three calls that the Web SDK's ad module makes for you, in the same order and with the same ad session ids.

It does not have what the Web SDK's ad module adds on top of those proofs:

  • The ad session object. There is no ads.session_opened(surface?, adUnit?), ad.id, ad.click(), ad.end() or ads.counts. Your app keeps DEA's ad session id for each ad itself and sends each proof itself.
  • The check that the user touched the ad. The Web SDK watches the ad's element and refuses a click that nothing touched.

Note: The click is not checked against touches on Flutter: your app declares it, and the SDK signs it. Send a click only from the handler of a real tap on the ad, at most one per ad, and none after the end.

Like the Web module, the calls do not read the classification: a bot result does not stop a proof, and the score inside the proof tells DEA how human the session was. But nothing is signed until the session is sufficient: until it has as many actions as the model asks for, 8 with the built-in model (see Evidence). Before that, each call answers that nothing was signed, so an impression reported then is not recorded, and the SDK does not keep it to send later. Check that getClassification().result is not insufficient before the impression when it has to be recorded — see Find out why a proof was not accepted.

When to use it

  • You sell ad inventory and want to show buyers verified human traffic
  • You want each impression, click and ad end recorded with the score at that moment

Prerequisites

  • The SDK is installed and initialized with this platform's appId, its appKey, and registryAppId set to the same appId. See the Quick Start and Accounts and keys.
  • The device key is registered: call attest() and get Verdict.passed before the impression. The proof calls do not register the key themselves, and DEA refuses a proof signed by a key it has not registered.

Pass all three to DeaSdk.init, each the value issued for the platform the app runs on:

import 'dart:io' show Platform;

import 'package:dea_sdk/dea_sdk.dart';
import 'package:flutter/material.dart';

void main() {
  DeaSdk.init(
    DeaOptions(
      appId: Platform.isIOS ? 'YOUR_IOS_APP_ID' : 'YOUR_ANDROID_APP_ID',
    ),
    appKey: Platform.isIOS ? 'YOUR_IOS_APP_KEY' : 'YOUR_ANDROID_APP_KEY',
    registryAppId: Platform.isIOS ? 'YOUR_IOS_APP_ID' : 'YOUR_ANDROID_APP_ID',
  );
  runApp(const MyApp());
}

Warning: The proof calls do not check for appKey or an attestation. Without appKey, attest() answers unsupported, and the proof calls still sign and send, with an app key that is not yours; DEA refuses them. Without a passed attestation, they still sign and send, and the proof claims tier C.

registryAppId is the appId written inside each proof. An app id that DEA's service sends for your app takes precedence over it. Left null, and when DEA sends none, proofs carry a built-in development id, which DEA refuses for your app.

Usage

The three proofs of one ad go up in this order, as the Web module sends them:

Step Event sessionId Sent as
The ad is on screen ProofEvent.adImpression '' (empty) POST /api/sessions. DEA answers with the ad's session id
The user taps the ad ProofEvent.adClick DEA's id POST /api/sessions
The ad is gone ProofEvent.adEnded DEA's id PUT /api/sessions/{id}, with update: true

The ad's session id is not the capture session: never pass getSessionInfo().session. sessionId has no default, so each call names it.

1. Send the impression when the ad is on screen

final dea = DeaSdk.instance!;

final attestation = await dea.attest(); // answers at once while the last registration is valid
if (attestation.verdict != Verdict.passed) return;

final outcome = await dea.sendProof(
  sessionId: '', // empty: DEA makes the ad's session id
  event: ProofEvent.adImpression,
  adUnit: 'home_banner',
);
final adSessionId = outcome.sessionId;
if (!outcome.accepted || adSessionId == null) return; // no session opened

sendProof() signs the impression, sends it, and completes with a RecordOutcome. When DEA accepts it, outcome.sessionId is the id DEA made for this ad; the click and the end are sent with it. Use sendProof() for the impression rather than record(), which answers only true or false and so cannot give you the id.

Pass the ad unit the ad loaded into as adUnit: it is signed into the proof as ad_unit. Pass the same value for the click and the end.

2. Send the click

From the handler of a tap on the ad:

await DeaSdk.instance?.sendProof(
  sessionId: adSessionId,
  event: ProofEvent.adClick,
  adUnit: 'home_banner',
);

3. Send the end

When the ad is removed, replaced, or its screen closes:

await DeaSdk.instance?.sendProof(
  sessionId: adSessionId,
  event: ProofEvent.adEnded,
  adUnit: 'home_banner',
  update: true, // PUT /api/sessions/{adSessionId}
);

update: true sends the end as an update of the session the impression opened. The SDK does not set it for you: pass it with every ProofEvent.adEnded.

Delivery and retries

  • Each call first asks DEA for a single-use nonce, which goes inside the signature. When DEA does not give one, the proof is signed without it.
  • When DEA cannot be reached or answers with a 5xx status, sendProof() and record() send the same proof again after 2 seconds, then 4, 8 and 16, plus up to 1 second, at most five sends in all. A 4xx answer is final.
  • A proof is valid for 90 seconds after it is signed, and no send starts after that. The future completes after the last send, so it can take more than a minute.
  • Nothing is kept on the device: a proof still unsent when the app is closed is lost.

Find out why a proof was not accepted

sendProof() never answers without a reason. When outcome.accepted is false, outcome.said says why, and outcome.isSigned says whether anything was signed:

isSigned said Why
false no device key on this handset No device key could be created or read on this device
false nothing signed: core refused -33: the session is not scored enough yet The session is not sufficient yet — fewer actions than the model asks for, 8 with the built-in model — which getClassification() reports as insufficient. Nothing was sent, so an impression is not recorded. Send it again once the session is sufficient, if the ad is still on screen
false nothing signed: no core loaded, so nothing is composed The scoring engine is not running: isScoring is false, and error says why
false Starts with nothing signed: the device key did not sign The platform refused to sign with the device key
true HttpException: /api/sessions… answered 4xx, then DEA's answer DEA refused the proof — for example because the device key is not registered (call attest() first), or appKey and registryAppId do not name the same app
true HttpException: /api/sessions… answered 5xx, or another error DEA failed or could not be reached on all five sends, or its answer could not be read
true proof expired before it was delivered DEA could not be reached before the proof expired, 90 seconds after signing

said is text for a person to read, such as on a diagnostics screen. When accepted is true, said is DEA's answer, or null when the answer was empty.

Note: An impression that DEA accepted can still leave outcome.sessionId null, when DEA's answer named no session id. No session opened: do not send the click or the end.

Sign without sending

getProof() signs the proof and returns it, as a SignedProof, without sending it. It takes the same sessionId, event, attestation, adUnit and appScopedUid as sendProof(), and returns null when nothing was signed, for the reasons in the table above.

final signed = await DeaSdk.instance?.getProof(
  sessionId: '',
  event: ProofEvent.adImpression,
  adUnit: 'home_banner',
);
final jws = signed?.proof; // compact JWS, valid for 90 seconds

The SDK does not retry or deliver a proof from getProof(). Use sendProof() unless you send proofs to DEA another way.

Read the classification before you request an ad

The proofs record ads you have already shown. To decide whether to request an ad at all, read the classification first, and decide what to do with it in your own ad stack — see Human Scoring.

Examples

One ad, from impression to end

import 'dart:io' show Platform;

import 'package:dea_sdk/dea_sdk.dart';

/// Call once in main(), before runApp().
void initDea() {
  final ios = Platform.isIOS;
  DeaSdk.init(
    DeaOptions(appId: ios ? 'YOUR_IOS_APP_ID' : 'YOUR_ANDROID_APP_ID'),
    appKey: ios ? 'YOUR_IOS_APP_KEY' : 'YOUR_ANDROID_APP_KEY',
    registryAppId: ios ? 'YOUR_IOS_APP_ID' : 'YOUR_ANDROID_APP_ID',
  );
}

/// The proofs of one ad: the impression, at most one click, and the end.
class AdProofs {
  AdProofs(this.adUnit);

  final String adUnit;

  /// DEA's session id for this ad, from its answer to the impression.
  String? _sessionId;
  bool _clicked = false;
  bool _ended = false;

  /// The ad is on screen. Returns why no session opened, or null when one did.
  Future<String?> shown() async {
    final dea = DeaSdk.instance;
    if (dea == null) return 'DeaSdk.init has not run';

    final attestation = await dea.attest();
    if (attestation.verdict != Verdict.passed) {
      return 'device key not registered: ${attestation.detail}';
    }

    final outcome = await dea.sendProof(
      sessionId: '', // empty: DEA makes the session id
      event: ProofEvent.adImpression,
      adUnit: adUnit,
    );
    if (!outcome.accepted) return outcome.said ?? 'not accepted';

    final id = outcome.sessionId;
    if (id == null) return 'DEA answered no session id';
    _sessionId = id;
    return null;
  }

  /// The user tapped the ad. Call it from your ad's click callback.
  Future<void> clicked() async {
    final id = _sessionId;
    if (id == null || _clicked || _ended) return;
    _clicked = true;

    await DeaSdk.instance?.sendProof(
      sessionId: id,
      event: ProofEvent.adClick,
      adUnit: adUnit,
    );
  }

  /// The ad is gone: removed, replaced, or its screen closed.
  Future<void> ended() async {
    final id = _sessionId;
    if (id == null || _ended) return;
    _ended = true;

    await DeaSdk.instance?.sendProof(
      sessionId: id,
      event: ProofEvent.adEnded,
      adUnit: adUnit,
      update: true, // PUT /api/sessions/{id}
    );
  }
}

Create one AdProofs per ad you show, and call shown(), clicked() and ended() from your ad SDK's callbacks for that ad. The next ad is a new AdProofs, and DEA gives it a new id.

API

Member Returns Description
sendProof({sessionId, event, …}) Future<RecordOutcome> Signs and sends one proof; the outcome carries DEA's session id, or why it was not accepted
record({sessionId, event, …}) Future<bool> The same send; true when DEA accepted the proof
getProof({sessionId, event, …}) Future<SignedProof?> Signs one proof without sending it; null when nothing was signed
ProofEvent — adImpression, adClick, adEnded

See the API Reference for every parameter.