Good fit when
- Ads render, and nobody can demonstrate which parameters the ad server actually received.
- Identity or signal enrichment was added, and the first ad request of a cold session was never checked.
- The app has more live placements than there are request sites anyone can point to in the code.
- Video was added to an app that already had display, and it behaves like an unrelated product.
- The release build behaves differently from the debug build everything was tested against.
How we build it
- 1Read the initialization path and list every ad-request site, per platform and per wrapper.
- 2Fix the ordering: consent, then adapter registration, then awaited initialization, then requests.
- 3Route every placement through one request builder so a new placement cannot be an unenriched one.
- 4Verify with real ad requests, captured and read, including the first request after a cold start.
Expected outcome
An integration whose request contents you can demonstrate instead of assume, with the silent losses — unenriched first requests and unenriched placements — closed.
Four systems that get talked about as one
Google Ad Manager, AdMob, IMA for video and GMA for mobile display are routinely discussed as a single interchangeable thing. They are not. Each has its own initialization order, its own request shape and its own failure modes, and treating them as one is the usual source of an integration that appears to work and earns less than it should.
The mobile display SDK serves both AdMob and Ad Manager, which is exactly why the confusion persists. The request objects are not the same: an Ad Manager request carries custom key-value targeting and publisher-supplied signals that an AdMob request has nowhere to put, and the two use different ad unit identifier formats. Code written for one and pointed at the other compiles, requests ads, renders ads, and drops the targeting on the floor.
Video is a different model again. IMA requests against an ad tag rather than an ad unit, gets back a VAST or VMAP response, and has to be wired into your content player: an ads loader, an ads manager, a display container over the video surface, and a scheduler that pauses content, plays the break, and returns control. Its failures look nothing like display failures, and none of them resemble a null pointer.
Where each one goes wrong first
Google Ad Manager
Targeting that never arrives, usually because it was set on the wrong request type or set after the request object was already built. The ad still fills, so nothing looks wrong from inside the app.
AdMob
Being treated as Ad Manager with a different account. Ad unit formats, targeting support and reporting granularity differ, and a migration done as a configuration change leaves capabilities silently switched off.
IMA
Player and ad lifecycle mistakes: an empty VAST response handled as a crash instead of a skipped break, content that never resumes, progress not reported so mid-roll breaks never trigger.
GMA
Requests fired before initialization completes. The initialization callback reports per-adapter readiness for a reason, and ignoring it means the first requests of the session go out against a partially ready stack.
Ordering: the mistake that costs money and reports nothing
Identity and signal enrichment has an ordering constraint that nothing in the toolchain enforces. The adapter or signal collector has to be registered and ready before the ads SDK initializes, and its readiness has to be awaited. Get that wrong and the first ad request of the session goes out unenriched. No exception is thrown. No log line appears. An ad is returned and displayed. The only symptom is that the session earned less than it should have.
This matters more than a normal bug because of which request is affected. The first request of a session is often the app-open or first-screen placement, which is frequently the most valuable one, and it is precisely the request that a warm-up race condition loses. QA cannot see it: an ad appeared, the screen looked right, the tester moves on.
Consent sits in the same sequence and is subject to the same trap. A consent decision collected after the ads SDK has already initialized and requested did not apply to those requests. On iOS, the tracking permission prompt is part of this ordering too. So the initialization path deserves to be written down as an explicit sequence and reviewed as one, rather than distributed across whichever lifecycle callbacks were convenient.
Awaiting readiness means awaiting it. A timer that waits a second and hopes is not a fix; it is the same bug with worse reproducibility, and it will behave differently on a cold start, on a slow network and on a low-end device. If requests can arrive before the stack is ready, queue them and flush the queue when the completion callback fires.
Every ad-request site, not the ones you remember
Enrichment has to be applied at every place the app requests an ad. Real apps accumulate request sites faster than anyone tracks: a banner in a list, an interstitial between two screens, a rewarded placement inside a game loop, a native unit in a feed, a pre-roll in the video player, and a second copy of one of those added behind a feature flag last quarter. Missing one degrades that placement quietly and permanently.
Nothing in the logs points at it, because there is no error. The request was well-formed and the ad server answered. This is why the durable fix is structural rather than diligent: one function builds every ad request, applies targeting and signals there, and is the only place a request object is constructed. Then coverage is a question you can answer by searching the codebase instead of by remembering.
Keeping coverage from drifting back
One request builder
Every placement gets its request from the same code path. Constructing a raw request anywhere else becomes the thing code review looks for, which is a much easier rule to hold than remembering to add three parameters.
A maintained placement inventory
Every ad unit and ad tag, per platform, with the screen it appears on. This list is also what makes ad server reporting comparable to what the app believes it is doing.
Check both sides of the bridge
React Native, Flutter and Unity apps frequently have a second request path in native code, added for one placement that the wrapper did not support. It is enriched separately or not at all.
Cold start as its own test case
The first request after a launch is a different scenario from the fifth. Treating them as one case is what lets the ordering bug live.
Verification means real requests, captured and read
There is no unit test for whether the ad server received the parameter you think you sent. A test can assert that your code put a value into an object. It cannot tell you that the value left the device, survived the SDK, and arrived. Between the assertion and the ad server there is a request builder, an SDK version, a mediation layer, a consent state and a network, all of which can drop a field without complaining.
So verification is empirical. Make real ad requests. Capture them. Read them. The SDK's own inspector and test device configuration cover the client side; a proxy shows what actually went out; for video, the ad tag response is the artifact to read, since an empty response and a broken player look identical from the couch. On the receiving side, the ad server's own reporting and diagnostics are what confirm arrival, which means this work needs somebody with access to that account, not only to the codebase.
Confirm the negatives as well. A placement showing nothing can be a no-fill, an error, or a request that was never made, and those three have completely different fixes while looking the same to a user staring at an empty rectangle. Distinguishing them is most of an audit.
How this work gets scoped
It usually starts with an audit rather than a rewrite, because reading the code tells you what somebody intended and only the requests tell you what happens. That pass produces three things: the actual initialization sequence, the real list of request sites per platform, and a set of captured requests showing what each placement sends. Then the fixes are small and specific, and they can be verified the same way they were found.
The multiplier is platforms. The same integration on Android, iOS and a cross-platform wrapper is three integrations with three sets of failure modes, and the Android side inherits the release-build problem: ad SDKs and mediation adapters get resolved by class name, so shrinking without correct keep rules produces an integration that works in debug and fails in the build you publish. That verification belongs to the same job.
Audit and fix work of this kind normally sits in hourly work at $85 to $165 per hour for hours actually worked. A full integration across platforms is scoped as a build instead, at $5,000 to $10,000 per estimated month of work, positioned in the range by how many platforms and placements are in scope.
Worked example: the order of operations for an enriched request
Almost every silently under-delivering integration has one of these steps in the wrong place. The sequence is short, which is what makes it easy to get wrong and easy to fix once it is written down.
- 1Resolve consent before anything requestsThe consent state has to exist before the first ad request, because a decision recorded afterwards did not apply to requests that already went out.
- 2Register the collector or adapter before initializingRegistration happens before the ads SDK initializes, not on the first screen that shows an ad. After initialization is too late for the requests already in flight.
- 3Await readiness on the callback, not on a timerUse the initialization completion signal, hold ad requests until it fires, then flush them. A fixed delay is the same race with a different disguise.
- 4Build every request through one pathA single builder sets targeting and signals for every placement, so adding a placement cannot mean adding an unenriched one.
- 5Cold start, capture, readLaunch from cold, capture the first request, and confirm the enrichment is on that one rather than only on the third. Then repeat per platform and per wrapper.
What this closes is the failure no dashboard reports, no exception surfaces and no test detects: a request that went out looking perfectly healthy and was worth less than it should have been.
Questions we get asked about this
Our ads work. Why would we audit them?
Because working and enriched are different states, and only one of them is visible. An ad rendering proves a request was made and filled. It says nothing about whether the request carried the targeting and signals you configured, and the case where it does not is not an error condition anywhere in the stack.
Can you tell from our code whether the integration is correct?
Only partly, and that is not a limitation we can engineer around. Reading the code shows the intended sequence and finds request sites. Whether the parameters survive the SDK, the mediation layer and the consent state is a property of the requests that leave the device, so real requests have to be captured and read.
Is moving from AdMob to Ad Manager a configuration change?
No. The identifier formats differ, the request types differ, and custom key-value targeting has nowhere to live on an AdMob request. A migration done purely in configuration typically ends up serving ads correctly with the targeting quietly absent, which is the hardest version of this problem to notice.
We are on React Native, Flutter or Unity. Does that change the work?
It adds the layer where most breakage happens. The wrapper has to forward calls and callbacks that the underlying SDK expects, including main-thread requirements for anything that creates an ad view. It is also common to find a native request path added alongside the wrapper for one placement, which is then enriched separately or not at all.
Do you work on the ad server side too, or only the app?
The app side is the engineering work. The ad server side is where arrival gets confirmed, so the verification pass needs somebody with account access working alongside us, whether that is your ad ops person or your network contact. An integration verified only from inside the app is half verified.
What about mediation adapters?
They are versioned artifacts of their own and have to match the SDK version they are used with, which makes an SDK upgrade a coordinated change rather than a single bump. The initialization callback reports readiness per adapter, and on Android each adapter is resolved by name, so they need keep rules in the release build like any other reflective lookup.
Typical deliverables
- Documented initialization and consent ordering, in code
- A single request path with enrichment applied everywhere
- A verification pass against captured real ad requests
- Per-platform integration notes and a failure-mode runbook
Best for
Apps and publishers whose ad stack is technically live but under-delivering, and teams adding video, identity or signal enrichment to an integration that already exists.
Discuss this service