Skip to content

Ads

Prove to your advertising partners that an impression or a click came from a real person. For each moment of an ad (on screen, clicked, gone) the SDK signs the current score into a proof with the device key, and sends it to DEA's recording service, which keeps it so it can be verified later.

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

Note: iOS has the proof calls, not the Web ad module. You send the three proofs yourself with sendProof, and the SDK does not check that the user touched the ad.

What iOS has

The Web SDK has an ad module: ads.session_opened(surface?, adUnit?) sends the impression and resolves an AdSession whose id, click() and end() carry the rest, ads.counts counts what happened, and a click is refused when nothing touched the ad's element. iOS has neither the module nor the touch check. It has the calls the module is built on: sendProof, record and getProof.

Web iOS
The ad is on screen ads.session_opened(surface?, adUnit?) sendProof(event: .adImpression, sessionId: ""), and keep the sessionId DEA answers with
The ad is clicked ad.click() sendProof(event: .adClick, sessionId: id)
The ad is gone ad.end() sendProof(event: .adEnded, sessionId: id, update: true)
The click is checked against touches Yes, when you pass the ad's element No: your app declares it
One click per ad, nothing after the end Enforced by the SDK Up to you
What happened ads.counts The RecordOutcome each call returns

When to use it

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

Prerequisites

  • The SDK is installed and started with your appId and appKey. See the Quick Start. Without appKey, nothing is sent.
  • registryAppId set to your appId. Proofs carry it as their appId, and DEA accepts a proof only when it names the app behind appKey. Left nil, proofs carry a built-in development id. See Accounts and keys.
  • The device key registered: call attest() and get .passed before the impression. The SDK does not register the key by itself and does not check that you did: without a registration, proofs are still signed and sent, and DEA refuses them, as it refuses every proof signed by a key it has not registered.
  • The user's consent, when requireConsent is on (the default). Before it, nothing is recorded, so the session never becomes sufficient and nothing is signed.

The Quick Start's holder, with registryAppId added:

import DeaSDK

enum Dea {
    /// The app's one DEA SDK instance. Nil until `start()` has run.
    private(set) static var sdk: DeaSdk?

    static func start() {
        guard sdk == nil else { return }
        let dea = DeaSdk(
            options: DeaSdkOptions(
                appId: "YOUR_APP_ID",
                appKey: "YOUR_APP_KEY",
                registryAppId: "YOUR_APP_ID"
            )
        )
        dea.start()
        sdk = dea
    }
}

Usage

sendProof makes network requests and blocks the calling thread until they finish. Never call it on the main thread.

1. Send the impression when the ad is on screen

import DeaSDK
import Foundation

DispatchQueue.global(qos: .utility).async {
    guard let dea = Dea.sdk, dea.attest().verdict == .passed else { return }

    let outcome = dea.sendProof(event: .adImpression, sessionId: "", adUnit: "YOUR_AD_UNIT")
    guard outcome.accepted, let adSessionId = outcome.sessionId else {
        print("DEA impression not recorded: \(outcome.said ?? "")")
        return
    }
    print("DEA ad session \(adSessionId)") // keep it for the click and the end
}
  • Pass sessionId: "". The impression is sent with an empty session id, and DEA answers with the id of the ad session it opened; outcome.sessionId carries it. Leave sessionId out and the SDK signs the id of the SDK's own session (sessionInfo.session) instead, which is not an ad session.
  • adUnit is signed into the proof as ad_unit. Pass the same one to the impression, the click and the end.
  • outcome.sessionId is nil when DEA did not accept the impression or its answer named no id. Then there is no ad session to send a click or an end against.

2. Send the click and the end with DEA's id

import DeaSDK
import Foundation

DispatchQueue.global(qos: .utility).async {
    guard let dea = Dea.sdk else { return }

    // The user clicked the ad.
    _ = dea.sendProof(event: .adClick, sessionId: adSessionId, adUnit: "YOUR_AD_UNIT")

    // Later: the ad is gone.
    _ = dea.sendProof(event: .adEnded, sessionId: adSessionId, adUnit: "YOUR_AD_UNIT", update: true)
}

adSessionId is the id from step 1. The impression and the click are sent as POST /api/sessions; update: true sends the end as PUT /api/sessions/{adSessionId}, the update of the ad session the impression opened. Send them in this order, each after the one before has returned: the Example below uses one serial queue for that.

Note: The click is not checked against touches on iOS. The SDK signs and sends a click whenever your app calls sendProof(event: .adClick, …), without looking at what the user touched, and it does not stop a second click or a click after the end. Your app declares the click: send one per ad, when your ad SDK reports it.

What each call does

  • sendProof signs one proof and sends it. It returns a RecordOutcome: whether DEA accepted the proof, the proof itself, what DEA said, and the ad session id DEA answered with.
  • record is the same call, and returns only whether DEA accepted the proof.
  • getProof signs one proof and returns it as a SignedProof without sending it, for an app that delivers proofs its own way.

Each call asks DEA for a single-use nonce before it signs, so all three block, getProof included. When DEA gives none (no appKey, no answer, or a refusal), the proof is signed without one. The proof carries the current score, written by the scoring engine and signed with the device key.

sendProof and record then send the proof. When DEA does not answer or answers with a 5xx, the SDK sends it again after 2, 4, 8 and 16 seconds (plus up to one second each), five sends at most, and never after the proof's 90-second lifetime. Each request can take up to 15 seconds, so a call can block for up to about two minutes when requests time out. A 4xx answer is final.

The attestation parameter decides only the tier signed into the proof. Left nil, the SDK uses the registration it holds: B when it passed with an App Attest token (platformAttested) and has not expired, C otherwise. extra adds fields of your own to the request body, beside the proof and outside its signature; DEA trusts nothing in it, and it never replaces the proof itself. Its values must be JSON values.

Read why a proof was not recorded

sendProof never throws. When accepted is false, said says why:

Why isSigned said
The session is not sufficient yet false nothing signed: core refused -33: the session is not scored enough yet
No device key could be created on this device false no device key on this handset
The scoring engine is not running (ready is false) false nothing signed: no core loaded, so nothing is composed
No appKey: nothing is sent true no appKey: a production account must be given the key it was issued, DeaSdkOptions.appKey
DEA refused the proof (a 4xx answer) true The path, DEA's status and its words, such as /api/sessions answered 401: …
DEA did not answer, or answered 5xx, five times true The last failure, such as unreachable: …
The proof expired before a send got through true proof expired before it was delivered

getProof returns nil in the first three cases. A session that has just started is not sufficient until it has as many actions as the model asks for, 8 with the built-in model (see Evidence). Until then nothing is signed, so an impression sent then is not recorded, and the SDK does not keep it to send later. Check that getClassification().result is not .insufficient before the impression, as the Example does, rather than spend a request on a refusal. When the impression is refused, there is no ad session for the click and the end.

Read the classification before you request an ad

You can also read the classification before you request an ad, and decide what to do with it in your own ad stack — see Human Scoring.

Examples

One ad, from on screen to gone

import DeaSDK
import Foundation

/// The proofs for one ad, from on screen to gone. Each method returns at once; the proofs are
/// sent in order on a background queue.
final class AdProofs {
    private let adUnit: String
    private let queue = DispatchQueue(label: "ad-proofs") // serial: keeps the three in order
    private var adSessionId: String? // DEA's id for this ad, from the impression's answer
    private var hasClicked = false

    init(adUnit: String) {
        self.adUnit = adUnit
    }

    /// The ad is on screen.
    func shown() {
        queue.async { [self] in
            // Nothing is signed before the session is sufficient, and DEA refuses a proof
            // from a device key it has not registered.
            guard let dea = Dea.sdk,
                let result = (try? dea.getClassification())?.result,
                result != .insufficient,
                dea.attest().verdict == .passed
            else { return }

            let outcome = dea.sendProof(event: .adImpression, sessionId: "", adUnit: adUnit)
            if outcome.accepted, let id = outcome.sessionId {
                adSessionId = id
            } else {
                print("DEA impression not recorded: \(outcome.said ?? "")")
            }
        }
    }

    /// The user clicked the ad. Only the first click is sent.
    func clicked() {
        queue.async { [self] in
            guard let dea = Dea.sdk, let id = adSessionId, !hasClicked else { return }
            hasClicked = true

            let outcome = dea.sendProof(event: .adClick, sessionId: id, adUnit: adUnit)
            if !outcome.accepted {
                print("DEA click not recorded: \(outcome.said ?? "")")
            }
        }
    }

    /// The ad is gone: removed, replaced or dismissed.
    func gone() {
        queue.async { [self] in
            guard let dea = Dea.sdk, let id = adSessionId else { return }
            adSessionId = nil // nothing more is sent for this ad

            let outcome = dea.sendProof(
                event: .adEnded, sessionId: id, adUnit: adUnit, update: true)
            if !outcome.accepted {
                print("DEA end not recorded: \(outcome.said ?? "")")
            }
        }
    }
}

Create one AdProofs per ad, and call its methods from your ad SDK's callbacks:

let ad = AdProofs(adUnit: "YOUR_AD_UNIT")
ad.shown()   // your ad SDK reports the ad on screen
ad.clicked() // it reports a click
ad.gone()    // the ad is removed or dismissed

The next ad is a new AdProofs, and its impression opens a new ad session with a new id.

API

Member Returns Description
sendProof(event:sessionId:attestation:adUnit:appScopedUid:extra:update:) RecordOutcome Signs and sends one proof, and says what happened. Blocking
record(event:sessionId:attestation:adUnit:appScopedUid:extra:update:) Bool The same, answering only whether DEA accepted it. Blocking
getProof(event:sessionId:attestation:adUnit:appScopedUid:) SignedProof? Signs one proof without sending it. Blocking

ProofEvent: .adImpression, .adClick, .adEnded.

RecordOutcome:

Field Type Description
accepted Bool Whether DEA accepted the proof
signed SignedProof? The proof that was signed, or nil when nothing was
isSigned Bool Whether a proof was signed
said String? What DEA answered, or why nothing was sent. nil when DEA accepted it with an empty answer
sessionId String? The ad session id DEA answered with. Set by an accepted impression

For SignedProof and the parameters, see the API Reference.