Skip to content

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 naked Authorization: <key> header, with no Bearer prefix in front of it.
  • The Reporting key rides as the api_key query 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_key
  • start and end (the literal end=now is accepted)
  • columns
  • format (we use JSON)
  • report_type (we use advertiser)

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_id in the columns list or the sales column 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 cpp and nc_d0_cpp columns 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 having filter 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:

  • /assetReport for preset ranges (yesterday, last_7d, last_month).
  • /assetAnalyticsReport for 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_id as a hashed hex value (something like 67cea9f458ccc1a639cb3c74da86ffdd).
  • The Campaign Management API uses a numeric id for that same set, and those are the IDs you see in the browser interface. It also carries a hashed_id alongside the numeric id.
  • Join on hashed_id, never on the numeric id, 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-data command, 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, {}, or None depending on the parser). Code that gates success on a returned id reports 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. status and audience_strategy are 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 /list endpoints 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 a roas_day_target of DAY0 or DAY7.
  • CPE, cost per event, paired with an event_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_url fully 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/update requires "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. A creative_set_url sent empty 500s.
  • creative_set/create needs the assets array inline or it returns a 400 (code 104003, "One or more selected medias are invalid"). campaign_id must be a string. A PAUSED status is honored on create, unlike the campaign-create status field, which is dropped (see above).
  • Creative sets take language targeting through a languages field: 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_id and 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_HTML asset's url is 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 limit param (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.

  • 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.