Ad Platforms · AppLovin
Last Updated: August 11, 2026
API and automation
The programmatic layer for AppLovin Ads: the Reporting API, the Campaign Management API, and how Tierra ingests the data into its own database. Read alongside the playbook for the platform model and vocabulary. Campaign structure and audience strategy live in strategy; budgets, scaling, and billing in media buying; KPIs, attribution windows, and dashboard-vs-API reads in reporting and tracking.
Two doors, one tool
The MCP (Model Context Protocol) connection and the campaign-management screen in the browser are the same system. One is reached programmatically, the other by clicking, and they act on the same objects. Anything you can do in the browser you can do over the API, and a write from either side shows up on both. Reads can briefly disagree (see the read-replica lag below), but there's no separate "API account" or "API state" to reconcile.
Authentication and keys
Three separate keys, one per surface, and they aren't interchangeable:
- The Manage (Campaign Management) key starts with
ak_and goes in a nakedAuthorization: <key>header, with noBearerprefix in front of it. - The Reporting key rides as the
api_keyquery param (see the Reporting API section). - The Event key authorizes the pixel and the lead-gen Conversion API (see that section below).
account_id is always a query param, on every Manage call. One quiet failure to guard against: a key that carries a trailing newline fails auth, which reads like a bad key when it's really a stray whitespace character. Strip the key before you send it.
Reporting API
AppLovin's Reporting API doc. Base URL is https://r.applovin.com/report. Required params:
api_keystartandend(the literalend=nowis accepted)columnsformat(we use JSON)report_type(we useadvertiser)
It defaults to real-time. Pass day_column=day for the cohort view, which is what Tierra pulls: each row's day is the click day, and recent rows keep growing as conversions mature. Conversion columns are available: sales, the rolling sales_«x» (0d through 28d), and the roas_0d..roas_28d, cpp_0d..cpp_28d, and revenue families.
New-customer columns are native for D0 and D7 (nc_d0_checkouts, nc_d7_checkouts, nc_d0_cpp, nc_d7_roas, and their percentage breakdowns). There is no native NC D28 column; derive it if you need it. The attribution-window model behind D0/D7/D28 is in reporting and tracking.
Two gotchas that fail silently, so lock them in:
- You must include
creative_set_idin thecolumnslist or thesalescolumn comes back 0 across every row, which reads like there were no sales. Add it even when you plan to aggregate up to the campaign level. This is Tierra's empirical finding, not something AppLovin documents. - All reporting data is in UTC. Normalize on pull, or a day boundary lands conversions on the wrong date and you chase a phantom mismatch against a client's local-time dashboard.
- iOS/SKAN traffic hides sub-campaign and creative-level detail under a 50-install threshold, so a thin iOS segment reads as blank rows rather than small ones. That's a privacy floor, not missing data.
- The
cppandnc_d0_cppcolumns can come back 0 even when there's real spend and real sales. Compute them yourself from cost and sales when that happens, rather than trusting the 0.
Practical tuning:
- Lookback is 90 days on the website-side endpoint, which is Tierra's surface. The app-side Reporting API caps at 45; not our surface.
- A
havingfilter slows the response and raises the odds of a timeout. - When you paginate with
limit/offset, sort is lexicographical, so pass an explicit deterministic sort or rows can skip or duplicate across pages.
Asset Reporting API
AppLovin's Asset Reporting API doc. Separate from the main Reporting API and narrower. Two endpoints:
/assetReportfor preset ranges (yesterday,last_7d,last_month)./assetAnalyticsReportfor a specific date range.
Lookback is 45 days. It has no conversion columns at all: you get impressions, clicks, CTR, and cost, and nothing else, which is why asset-level work is managed against account-level KPIs rather than per-asset conversion (see the asset note in the playbook and creative). Yesterday's asset data is stable after 6:00am UTC.
Reporting IDs versus Campaign Management IDs
The Reporting API and the Campaign Management API name the same objects with different IDs, and joining on the wrong one returns zero matches with no error. It's not a trap, just something to know:
- The Reporting API returns
creative_set_idas a hashed hex value (something like67cea9f458ccc1a639cb3c74da86ffdd). - The Campaign Management API uses a numeric
idfor that same set, and those are the IDs you see in the browser interface. It also carries ahashed_idalongside the numericid. - Join on
hashed_id, never on the numericid, and the rows line up. - Campaign IDs are the exception: they're numeric in both APIs and match, so campaigns join cleanly. Only creative-set IDs are hashed on the Reporting side.
How Tierra ingests AppLovin data
Tierra doesn't read AppLovin live for reporting. A daily job pulls the data into Tierra's own database (Supabase), and the dashboard and the MCP read from there.
The pipeline:
- A daily scheduled job pulls a rolling last-30-days window for every active AppLovin account and upserts it into the database. Re-pulling the whole trailing 30 days each day is deliberate: late-maturing D7 and D28 conversions overwrite the earlier numbers, so the window stays correct as attribution matures.
- An on-demand pull handles custom date ranges and historical backfills. The team triggers it through the Slack
/pull-datacommand, which runs the pull per account. - History older than the API window is loaded from manual CSV exports, clipped to dates older than the API's reach so the recent window stays entirely API-owned (no date has both an export row and an API row).
What the daily job pulls (the creative-set report): day, campaign_id_external (stored as the campaign ID), campaign, creative_set_id, creative_set, impressions, clicks, cost, and the sales, roas, and total_rev columns at 0d, 7d, and 28d. It requests report_type=advertiser, JSON, not_zero, and day_column=day (cohort). It does not pull country or hour.
Learnings from building the ingestion:
- Grain has to include the campaign. The same creative set can run under more than one campaign on the same day with different metrics. Keying rows on date plus account plus creative set alone summed across campaigns and, for assets, collided on a duplicate key that failed the whole batch and dropped an account's data. The grain includes the campaign ID to fix it.
- Reconciliation "failing" by a few dollars is normal. AppLovin's account-level total differs slightly from the sum of its creative-set breakdown. It's intrinsic to AppLovin, not a sign of a problem, so don't chase it.
- Any paginated pull needs a stable sort. The database caps a response at 1000 rows, and paginating without an explicit ORDER BY silently skips or duplicates rows and produces wrong totals.
- Chunk long date ranges into 30-day pieces. The API's request window is 45 days, so 30-day chunks stay well inside it.
- Rate-limit and fail safely. The pull sleeps between calls and chunks, aborts an account after three consecutive chunk failures, finishes the current chunk on a stop signal rather than dropping mid-write, and auto-clears stale ingestion locks older than two hours.
Where it runs: on the DigitalOcean server at /opt/tierra-ingestion, daily via cron (a batch runner pulls AppLovin, then Google, then Meta). The on-demand pull is spawned by the Slack bot per account. Deploys are a local push then a pull on the server; the scripts are stateless, so there's nothing to restart.
Campaign Management API base and access
Base URL is https://api.ads.axon.ai/manage/v1/. Every response carries an X-TRACE-ID header. Log it, because AppLovin support triages by that ID.
A 403 on a brand-new account almost always means the Campaign Management entitlement isn't switched on yet, not that your key is wrong. Email the AppLovin rep with the trace ID to have it enabled (roughly four business days), and use the browser campaign-management screen in the meantime. Same objects, same writes, so nothing is blocked while you wait.
Campaign Management API write semantics
Writes behave in ways that break naive success checks. Three to plan around:
- An empty 200 response means success. A create returns HTTP 200 with an empty body (
null,{}, orNonedepending on the parser). Code that gates success on a returnedidreports failure while the object was actually created. Treat an empty 200 as probable success and confirm by looking the object up by name. - Some fields are silently ignored on create.
statusandaudience_strategyare dropped, and the new campaign reads back as LIVE and UNIVERSAL no matter what you sent. Follow the create with an update that sets the fields you actually wanted (name, type, status, audience_strategy) in one call. - The
/listendpoints read from a replica that lags the write. Right after a bulk create, a verify-by-list pass can miss the most-recently-created tail because the replica hasn't caught up, and retrying then produces a duplicate. Pause before the first list scan, retry after a longer pause, and prefer a single-id lookup for stragglers before declaring anything failed.
Optimization goals
Three goal types, not two:
CHK_ROAS, the ROAS goal, paired with aroas_day_targetofDAY0orDAY7.CPE, cost per event, paired with anevent_target.CPP, the Cost-Per-Purchase goal.
Billing mode is AUTO_BIDDING_WITH_CPM_BILLING across all three.
Creative-set writes
Setting a creative set's URL and status has its own set of quiet traps:
creative_set_urlfully REPLACES the campaign's website URL, query string and all. There is no parameter merge. Set a creative-set URL without the full tracking query and it lands with zero tracking, so every explicit creative-set URL must carry the complete campaign query template. utm templates owns the URL hierarchy; this is the API-level reason a partial URL silently breaks tracking.creative_set/updaterequires"type":"WEB"in every request body, even a status-only change like a pause. Omit it and the call 400s, so a safety pause silently no-ops and the set keeps spending. Acreative_set_urlsent empty 500s.creative_set/createneeds theassetsarray inline or it returns a 400 (code104003, "One or more selected medias are invalid").campaign_idmust be a string. APAUSEDstatus is honored on create, unlike the campaign-createstatusfield, which is dropped (see above).- Creative sets take language targeting through a
languagesfield: uppercase English names like["SPANISH"], and ISO codes 400. Language is a per-set lever, so a second language doesn't force a separate campaign the way geography does. It's a real targeting knob, not just a display setting.
Assets: upload, immutability, and classification
Assets are the most surprising corner of the API, so plan for the mechanics rather than assuming they behave like ordinary file uploads.
Upload reliability:
- An upload can return HTTP 200 with a valid
upload_idand still silently drop the asset: 30 to 50% never appear in the asset list. Upload, wait about 30 seconds, confirm the asset is listed by its name stem, and retry up to three times. Keep concurrent upload workers at three or fewer. - AppLovin silently dedupes byte-identical uploads. A control or comparison asset needs a tiny innocuous change (a microsecond of added duration, for example) to register as its own distinct asset.
Immutability:
- Assets have no update endpoint and no delete endpoint. Every fix ships as a brand-new rebuilt asset, and rejected assets persist in the list. That's the mechanical reason you can't edit your way out of a rejection, there's nothing to edit.
- A
HOSTED_HTMLasset'surlis a recovery path. The live HTML of any uploaded interactive re-downloads from the platform CDN, so a lost local build isn't fatal.
Type classification:
- A PNG static auto-classifies as
IMG_ICON. Upload statics as JPG to avoid that. - To have a static treated as an interactive or end card, wrap it as
HOSTED_HTML(.html). - Non-self-contained HTML, meaning external references or lazyloaded assets, also reclassifies to
IMG_ICON. So every piece of media inside an interactive's HTML must be embedded (base64), never linked.
Reference detail:
- Asset types:
VID_LONG_P,VID_SHORT_P,HOSTED_HTML,IMG_INTER_P,IMG_BANNER. - Upload limits: up to 40 files per request, up to 10 GB total, up to 1 GB per single file.
- Pagination returns 100 per page, and the asset-list endpoint ignores a
limitparam (it's fixed at 100 per page), so paginate until a page comes back empty. Id lists cap at 100.
The lead-gen Conversion API
AppLovin's Conversion API for lead gen doc. Lead-gen funnels (the webinar and similar) use a different event vocabulary than the ecommerce purchase funnel. The events are page_view and generate_lead, not the standard purchase-funnel events. generate_lead carries { "currency": ISO-4217, "value": number }, where the value is the lead's worth to the business, not a sale amount.
Endpoint is POST https://b.applovin.com/v1/event, authorized by header, with the pixel_id query param set to the AppLovin Event Key. Up to 100 events per batch. As with the ecommerce pixel, user_data is an object with sub-fields, not a flat value, and matching a browser event to its server-to-server twin means giving both the same dedupe id. The ecommerce pixel doctrine is in reporting and tracking.
Related references
- reporting and tracking: KPI definitions, attribution windows, pixel install, dashboard vs API reads.
- media buying: programmatic budget changes, timing, billing mechanics.
- strategy: campaign structure and audience strategy the Campaign Management API writes against.
- utm templates: the URL and tracking-query hierarchy that a creative-set URL must carry in full.
- creative: asset roles and creative workflow behind the asset endpoints above.