Mobile apps
Send app_installed and app_opened, the Conversions-API-only events for iOS and Android app campaigns.
app_installed and app_opened are Conversions API only, because no browser pixel can see an install. That changes the shape of the integration: everything is server-side, and the click ID has to survive an app-store round trip.
Event priority
| Priority | Event | Fires when | Why it matters |
|---|---|---|---|
| 1 | app_installed |
First launch after install | The conversion your install campaigns optimize toward. |
| 2 | app_opened |
App launches (throttled) | A retention signal. Distinguishes installs that stick from install-and-delete. |
| 3 | In-app conversions (trial_started, subscription_created, order_created) |
A purchase or subscription happens in the app | Revenue attribution. These are the same events as SaaS and ecommerce, sent from your app backend. |
The attribution chain
The web-to-app handoff is the hard part. Here is the full chain:
ChatGPT ad → landing page (Lapis pixel sets lapis_sid)
→ App Store / Play Store link
→ install → first launch → your backend
→ POST /pixel/server-events with the original lapis_click_id
The click ID has to cross the store boundary. There are two workable patterns.
Pattern A: deferred deep link (recommended)
If you use Branch, AppsFlyer, Adjust, or Firebase Dynamic Links, attach the click ID to the store link on the landing page. The deep-link tool then returns it on first launch.
<script>
(function () {
var cookie = document.cookie.split("; ").find(function (entry) {
return entry.indexOf("lapis_sid=") === 0;
});
if (!cookie) return;
try {
var sid = JSON.parse(
decodeURIComponent(cookie.split("=").slice(1).join("="))
).sid;
if (!sid) return;
document.querySelectorAll("a[data-store-link]").forEach(function (link) {
var url = new URL(link.href);
url.searchParams.set("lapis_click_id", sid); // your deep-link tool passes this through
link.href = url.toString();
});
} catch (e) {}
})();
</script>
On first launch, read lapis_click_id from the deferred deep-link payload, send it to your backend with the device’s first-launch call, and fire app_installed from there.
Pattern B: no deep-link tool
Without deferred deep links, the store boundary is a wall. Fall back to a web checkpoint: have the ad’s landing page capture a lead or account before the store handoff (for example, an email signup that returns an app invite link). The web conversion (lead_created or registration_completed) becomes your attributed event, and the install itself goes unattributed. Be honest about this in your reporting rather than inventing click IDs.
The events
Both events require action_source: "mobile_app" in properties. Lapis forwards it to OpenAI, whose API requires it for these events.
app_installed
Send once per device or account, on first launch:
{
"event_id": "app_installed:device_a8c2f",
"event_type": "app_installed",
"lapis_click_id": "<from deferred deep link>",
"properties": {
"action_source": "mobile_app",
"platform": "ios"
}
}
Event ID convention: key on your install or device identifier so reinstalls on the same device deduplicate. If you only have account-level identity, app_installed:<account_id> is fine.
app_opened
Do not send every open, because that is noise, not signal. Common practice is to send the first open per day, or opens on days 1, 7, and 30:
{
"event_id": "app_opened:device_a8c2f:2026-07-16",
"event_type": "app_opened",
"lapis_click_id": "<same as install>",
"properties": {
"action_source": "mobile_app",
"platform": "ios"
}
}
Note the date in the event ID. That is what makes “once per day” idempotent, no matter how many times your client fires it.
In-app revenue
Subscriptions and purchases inside the app use the standard events: subscription_created from your RevenueCat, StoreKit, or Play Billing webhook, and order_created for one-off purchases. Use the same lapis_click_id you captured at install. Key the IDs to the store transaction ID: subscription_created:txn_2000000123456789.
Warning: The Lapis attribution window is 30 days from the ad click. Installs usually convert to paid well inside that. If your app monetizes slowly, treat
app_installedas the optimization signal and accept that some revenue events land unattributed (202). They are still stored.
What good looks like
Read the funnel left to right. Each number is how many reached that stage.
sessions → store clicks → app_installed → app_opened (d1) → subscription_created
900 → 420 → 180 → 95 → 14
app_installed far below store clicks is normal (store-page drop-off). Zero app_installed with healthy store clicks means the deferred deep link is not returning the click ID. Test Pattern A’s payload end to end on a fresh device before launch.