26 October 2026 · 9 min read

Getting a List of Users Out of GA4: The Audience Export API

Build an audience in GA4 and the interface reports a size. It does not hand you the people. For remarketing inside Google Ads that is enough, because GA4 pushes membership across continuously and there is nothing to request. For anything outside Google's ad products, a count is not something you can act on.

The Data API is the way out. An audience export is a request for the users in one audience at one moment in time. You create it, you wait, you query it for rows. Two versions are in play: the export itself lives in v1beta, while recurring lists and webhooks live in v1alpha, which means Google can still change them.

The audience page gives you a number, not a file

Creating an export is one POST to the property's audienceExports collection.

Two fields are required. The first is audience, formatted as properties/1234567/audiences/12345. You get that name from the Admin API's audiences.list method, or from the audience URL in the interface. The second is dimensions, and it is a short list, which is the subject of the next section.

The response returns a resource name such as properties/1234567/audienceExports/123 and a state of CREATING. You then poll audienceExports.get with that name until the state reads ACTIVE or FAILED. audienceExports.list shows every export on the property, which is useful when a scheduled job leaves orphans behind.

Once the export is active, audienceExports.query returns the users. Rows arrive as audienceRows, each holding dimensionValues, with a rowCount alongside. That is the whole surface: create, get, list, query.

Four dimensions, and the one that decides who you may target

An audience export accepts deviceId, userId, isAdsPersonalizationAllowed and isLimitedAdTracking. Nothing else. This is not the reporting dimension catalogue, and asking for page_title returns an error rather than a column. If you need page level detail, you are joining exports to reporting data on your own side.

deviceId is a pseudonymous browser or app identifier. It is not a person and it does not survive a device change. userId returns whatever you set in tagging, whether or not user provided data collection is switched on. If you never send a user ID, that column comes back empty and the export is a device list.

The flag worth opening first is isAdsPersonalizationAllowed. It returns true, false or (not set). Google's instruction for (not set) is unusually direct: treat it as false for personalised ads, because some of those rows hold users who are not eligible. Only rows reading true are safe to push into an ad platform for personalised targeting.

A false row is not rubbish. Google states those users remain usable for non advertising work such as A/B testing and data exploration. isLimitedAdTracking carries the device's own Limit Ad Tracking setting, also with three possible values.

The clock: 24 hours of lag, a snapshot, then 72 hours

Audience export data freshness averages 24 hours. Google's worked example is worth memorising: a user makes their first purchase, you request the Purchasers audience seven hours later, and that user is probably absent. Between 14 and 38 hours after the purchase, the same user is likely to appear in a new export.

Membership is refreshed once a day, typically during daylight hours in the property's timezone. Two exports from the same morning usually return identical data, which is why Google recommends one export per day per audience rather than a frequent poll.

Each export is a static snapshot of the audience as it stood about 24 hours before the request. Nothing inside it updates. And it expires after 72 hours, so the pipeline shape is fixed: create, query, store, delete, repeat. Any script that queries an export a week later gets an error, not a stale list.

None of this applies to Google Ads remarketing, where GA4 exports members continuously and the API is never called. If Ads is your only destination, you can close the tab here.

When the list comes back sampled

Large audiences return a representative sample rather than every user. On a standard property the ceiling is 2 million returned users against 10 million considered users. Analytics 360 lifts those to 200 million and 1 billion. The limits are applied per request, not per property per day.

Returned users are the members in the export. Considered users are the wider pool the sample is drawn from. The practical consequence is that a row count near the ceiling is a sample, so it should not be used to reconcile revenue, to size a discount, or to bill a client.

Why the export is smaller than the report

Three separate mechanisms, and all three catch people out.

Reporting identity is ignored. An export always identifies a user by user ID first, then device ID, whatever the property setting says. A property on device based reporting does not deduplicate by user ID in its reports, so the report can legitimately show more users than the export returns.

Backfill is missing. When you create an audience and switch on Google Ads remarketing, GA4 backfills recent members into Ads. Reports and exports do not contain those backfilled memberships. A user has to log an event after the audience exists to appear. Google's example runs a 20 June purchase against a 21 June audience, with the user appearing on 22 June only if they logged an event on the 21st.

Membership duration truncates the history. The audience report's total users metric counts historical memberships at the time of each event. An export holds only memberships that have not expired. With a 30 day duration and a year of reporting, Google's own arithmetic puts the interface up to 12 times higher, because 365 divided by 30 is 12. Both numbers are right and they answer different questions.

Daily lists without a cron you have to trust

Requesting an export by hand each morning works until someone forgets. The v1alpha recurringAudienceLists.create method produces one new list per day for as long as the configuration stays active. It takes the same two fields, an audience name and a dimension list.

The field that governs everything is activeDaysRemaining. It counts down by one each day, and at zero no further lists are created. On a standard property the default is 14 days and the maximum is 30. Analytics 360 defaults to 180 and allows up to 365. The minimum is 1, and a request above the maximum is reduced to it rather than rejected.

The configuration returns audienceLists, an array of instance names ordered newest first. Those names are what you pass to audienceExports.query, so the retrieval step is identical to the one-off case. If your pipeline assumes the list will keep arriving forever, a standard property will stop feeding it after a fortnight.

Webhooks instead of polling

Polling the configuration costs almost nothing, and for one audience it is the sensible choice. Webhooks remove the guesswork.

Add a webhookNotification object with a uri and, optionally, a channelToken. The token comes back in the X-Goog-Channel-Token header, which is how you tell your own configuration from anything else posting at your endpoint.

One or two POSTs arrive per list. The first shows the new list in CREATING. The second follows when it reaches ACTIVE or FAILED. The endpoint has to answer quickly: Google confirms the webhook responded in under 5 seconds at creation time, and a bad address fails the create call with an INVALID_ARGUMENT error naming the status code it received. Each body carries the operation resource and a sentTimestamp in microseconds, which is what you compare to spot a replay.

Read that as an ordering constraint. The webhook has to be live before the list exists. An endpoint that sleeps, or one behind a queue that takes ten seconds to wake, blocks the call that creates the recurring list in the first place.

What it costs in quota

Creating audience exports draws on Core quota, the same bucket as runReport. A standard property gets 200,000 Core tokens per day, 40,000 per hour, 14,000 per project per property per hour and 10 concurrent requests. Analytics 360 gives 2 million, 400,000, 140,000 and 50.

One export per day per audience will not trouble any of that. A scheduled loop across every audience on a large property might, and the hourly ceiling is the one that bites first.

The point where the list stops being Google's problem

An export turns an audience into a file of identifiers on your infrastructure. From that moment it is your data, your retention rule and your lawful basis, not Google's.

Two habits follow. Filter on isAdsPersonalizationAllowed before the list reaches any ad platform, and treat (not set) as a no. Then decide how long a stored copy lives before you build the pipeline at all, because a daily list with no deletion rule turns into an archive nobody intended to keep.

The ICO's storage and access guidance is clear that advertising needs consent and that nothing non-essential runs before it. A list assembled to target people who declined is a list that should not exist. The API will happily build it for you, which is exactly why the check belongs in your process rather than in Google's.

Four checks before you build anything

Pick one audience with a meaningful size. Run a single export. Answer four questions.

Does the create call succeed, and what state does get report ten minutes later. How many rows come back, and how does that compare with the number the interface shows. What share of rows reads true on isAdsPersonalizationAllowed. What was the creation time, and when does the 72 hour clock run out.

Those four answers decide the rest. A one-off export fits a quarterly review. A recurring list with a webhook fits a daily activation pipeline, provided the property tier gives you enough active days. And if the destination is Google Ads remarketing, the honest answer is that you do not need this API at all.

If you need GA4 audiences in a warehouse, a CRM or an ad platform that Google does not feed, book a free tracking audit and we will check the export path, the sampling and the consent flags before you build it.