Soku AI
All blog posts

ChatGPT Ads Custom Audiences Can Now Be Edited — but Only the Developer Docs Know It

August 26, 2026 · 16 min read

Soku Team

Soku Team

ChatGPT Ads Custom Audiences Can Now Be Edited — but Only the Developer Docs Know It

On 2026-08-25 OpenAI added a dated entry to the ChatGPT Ads API changelog:

"Added custom audience Add, Remove, Replace, and Merge operations, automatic identifier matching, and support for small and empty exclusion only audiences."

That single line undoes the two rules that shaped how anyone sensible has been building ChatGPT Ads audiences since the feature launched: that an audience is immutable once created, and that an audience is useless below 25,000 matched users.

It also creates a problem. The ChatGPT Ads help centre still states both of the old rules, in plain language, on a page that was live when we captured it on 2026-08-26. OpenAI has changed the product in one documentation surface and not the other.

This page covers what the API now does, in OpenAI's own words, where the two surfaces disagree, and the operational rules — revisions, idempotency keys, conflict codes — that decide whether your audience maintenance is safe to automate.

All quotations are from official OpenAI documentation captured on 2026-08-26. Sources are linked at the end. Nothing here is inferred from an announcement email; there was no ChatGPT Ads product-update email in this window.

The two surfaces, side by side

This is the part to read before anything else, because it determines which documentation you should be building against.

The help centre, in Set up custom audiences for your campaign, still says:

"Custom audiences cannot be edited after creation. To change an audience list, create a new audience and archive the old one."

and

"Note that each custom audience must include at least 25,000 matched users before it can be used. We recommend audiences of at least 100,000 users."

The developer documentation, in Custom Audiences, says:

"Create an audience from a file or start with an empty audience, then add or remove customers as your list changes. You can also replace the full list or merge existing audiences into a new audience."

and

"Small audiences, including empty audiences, can be used for exclusion once they are ready. Inclusion and bid adjustments still require enough matched users."

These are not two descriptions of the same behaviour at different levels of detail. They are contradictory claims about what the product does.

The reconciliation that most likely applies — and we are labelling this as our reading, not OpenAI's statement — is that the help centre documents the Ads Manager interface, where audience membership is genuinely not editable, while the developer documentation documents the Ads API, where it now is. OpenAI does not say this anywhere. Until it does, the safe operating rule is:

  • Building through the API? The developer documentation is authoritative.
  • Working in Ads Manager? Assume the help centre still describes what you will see on screen.
  • Writing a process document for a team that uses both? Say which surface each step applies to, because the answer to "can I edit this audience" is now genuinely different depending on where you are standing.

What actually changed: four operations instead of one

Before this update, the audience lifecycle was create-and-archive. Now there are five distinct actions, and OpenAI tabulates them:

GoalInputResult
CreateUploaded file, or a name without a fileA new audience ID
AddInline identifiers or an uploaded fileAdd matched users to the same audience
RemoveInline identifiers or an uploaded fileRemove matched users from the same audience
ReplaceUploaded file containing the full desired listReplace membership while keeping the audience ID
Merge2 to 64 existing audience IDsA new, independent union audience

The phrase carrying the most weight is "while keeping the audience ID". OpenAI is explicit about the consequence:

"The audience keeps its ID and existing campaign and ad-group references."

That is the difference between a maintainable audience and a disposable one. Under the old model, refreshing a suppression list meant creating a new audience, then editing every campaign and ad group that referenced the old one, then archiving it. Under Replace, the campaign never knows anything happened.

Add and Remove are deltas, Replace is a snapshot

OpenAI draws this line clearly, and getting it wrong is the easiest way to destroy an audience:

"An Add file contains only customers to add. A Remove file contains only customers to remove; members omitted from the file remain in the audience. Use Replace when the file is a full snapshot of the desired membership."

The behaviour is idempotent in the useful direction — "Add doesn't duplicate a user who is already a member. Remove doesn't change membership for an absent user" — so re-sending a delta that has already landed is not destructive.

There is also an explicit anti-pattern, which is worth quoting because it is exactly what a naive implementation does:

"Don't emulate replacement by removing every member and adding them back."

Inline for small changes, files for bulk

Add and Remove accept either an uploaded file_id or an inline identifiers array, and inline entries can mix all five identifier types, each carrying its own identifier_type:

curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/add" \
  -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: custom-audience-add-001" \
  -d '{
    "expected_revision": 0,
    "identifiers": [
      { "identifier_type": "email", "identifier": "[email protected]" },
      { "identifier_type": "gaid", "identifier": "38400000-8cf0-11bd-b23e-10b96e40000d" }
    ]
  }'

The bounds are specific: batches up to 10,000 inline identifiers use the small-update path, larger inline batches fall back to file-based processing, and the whole request body must fit within 16 MiB or the API returns 413. OpenAI notes that 10,000 "is not a hard item-count limit" — it is the threshold where processing changes character.

Mixed-identifier CSVs: identifier_resolution: "auto"

The second half of the changelog line — "automatic identifier matching" — is a real reduction in ETL work.

Previously a file carried one identifier type, declared in identifier_type. Now a single CSV can carry all of them:

email,phone_number,email_sha256,phone_number_sha256,gaid
[email protected],+12025550123,,,
,,057a0fff4c78ae3e14236c36b611061cbdd54ccd72a34b23f77d7a8c4bca4963,,
,,,1a2d415d4fef1dfafe57e0d98af15bbad8cc4bd8ca8ac66e89f2e0ef3941d500,
,,,,38400000-8cf0-11bd-b23e-10b96e40000d

Set identifier_resolution to auto and OpenAI resolves the columns:

"Each populated identifier cell is a matching candidate; a row doesn't require every identifier to match the same user. OpenAI counts each matched user once, so different identifiers can represent one audience member."

That last clause matters for anyone who has been maintaining separate email and phone audiences to widen match rates: the deduplication is handled, so the merged file does not inflate your count.

The size trade-off is documented and slightly counter-intuitive:

"Without it, use a single identifier type and specify that type in identifier_type; this single-type processing path accepts up to 5,000,000 identifiers. Automatic resolution supports larger files within the same 500 MB upload limit."

So the single-type path has a hard item ceiling of five million; the auto path is bounded only by the 500 MB file size. For a large first-party list, auto is the path with more headroom, not less.

The hashing rules have not changed, and they are still where uploads fail

Worth restating because they are unchanged and still unforgiving:

  • Before hashing an email: trim whitespace, lowercase. Do not strip dots or plus tags.
  • Before hashing a phone: normalise to E.164 including + and country code.
  • Send the 64-character hexadecimal digest, not Base64, hashed over the UTF-8 value with no trailing newline.
  • GAIDs must be raw, nonzero, hyphenated UUIDs. Do not prehash GAIDs — OpenAI normalises and hashes them internally.

Small and empty audiences: exclusion is now a separate class

This is the change most likely to alter what campaigns you can actually run.

The old rule was flat: 25,000 matched users or the audience is unusable. The new rule is per-use, and OpenAI tabulates it:

Intended useSize requirement
exclusionReady small or empty audiences can be used. No minimum matched size is required.
inclusionThe audience must meet the matched-user minimum.
bid_multiplierThe audience must meet the matched-user minimum for bid adjustments.

For a mid-sized advertiser this is the difference between being able to suppress your existing customers and not. A B2B company with 4,000 customers could not previously build a suppression list at all — it fell under the floor. It now can.

The 25,000 figure survives, but demoted to a planning number for the other two uses:

"For inclusion and bid adjustments, use 25,000 matched users as the public planning threshold. Privacy safeguards can affect the exact boundary, and uploading 25,000 identifiers doesn't guarantee enough matched users."

Two operational warnings come with it. First, there is no creation-time flag for this:

"Don't send an exclusion_only creation field: eligibility depends on how you use the audience."

Second — and this is the one that will catch integrations — status: "ready" does not mean eligible:

"Don't infer bid eligibility from ready status or a matched count range. The server validates eligibility when you save the ad group."

The supported way to ask is an explicit query:

curl -G "https://api.ads.openai.com/v1/custom_audiences" \
  -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \
  --data-urlencode "intended_use=exclusion" \
  --data-urlencode "custom_audience_ids[]=caud_123"

The response returns only eligible audiences plus a policy_revision token, which you send back to recheck a selection. OpenAI is careful to warn that this token is a distinct thing from the membership revision: "policy_revision is not membership_revision or a campaign-write parameter." Confusing the two produces 409 custom_audience_policy_revision_mismatch.

Concurrency: revisions, idempotency keys, and five ways to get a 409

Mutable audiences introduce a class of problem immutable ones did not have: two things changing the same list at once. OpenAI's answer is optimistic concurrency plus mandatory idempotency, and the rules are strict enough to be worth following literally.

Every membership operation requires an Idempotency-Key header. The warning attached to it is the important part:

"Reuse the key only to retry the same operation; retries return or resume the first accepted input. Don't change the file, identifiers, or revision under an existing key: a repeated key can return the original operation without checking the new body."

In other words, a key that gets recycled across two different updates will silently apply the first one twice. That is a data-integrity bug that looks like a successful API call.

Replace requires expected_revision. Read the current membership_revision, submit it with the Replace, and a stale value earns 409 custom_audience_replacement_revision_conflict.

The full set of documented failure responses:

ResponseWhat it means
409 custom_audience_operation_recovery_requiredAn Add/Remove was interrupted and may be partially applied. Resend the original POST with the same body and key, then poll the same operation.
409 custom_audience_mutation_conflictCompeting work was in flight; the edit was not applied. Wait, re-read state, reconsider.
409 custom_audience_replacement_revision_conflictYour expected_revision was stale. Refresh it.
409 custom_audience_policy_revision_mismatchEligibility was rechecked against an out-of-date policy_revision. Refresh without the old token.
503 custom_audience_operation_unavailableStatus temporarily unavailable. Retry with backoff — don't assume failure.
429Back off, retaining the original key for an accepted mutation.

The recovery guidance for a partially-applied operation is explicit about what not to do:

"Don't use a new key, replay the entire job as new work, or submit an inverse update to guess at recovery."

Note also that custom_audience_mutation_conflict is not confined to the audience endpoints. The campaigns reference now warns that it can surface on a campaign edit:

"A concurrent membership update can return 409 custom_audience_mutation_conflict without applying the campaign edit."

So a nightly audience refresh running against the same account as a campaign-management job can cause the campaign job to silently fail. If you are automating both, they need to be sequenced.

Polling is privacy-safe by design

Each operation returns an object with only four fields — operation_id, custom_audience_id, operation, status — and OpenAI is direct about the omission:

"The response exposes only the operation ID, audience ID, operation type, and status; it doesn't return raw identifiers, matching counts, or individual membership outcomes."

There is a related trap on Replace:

"The audience can still show ready during replacement. Poll the returned operation to determine when the replacement finishes, rather than relying on audience status alone."

An integration that waits on status == "ready" will conclude a Replace is done before it is.

Merge is a snapshot, not a subscription

Merge looks like the most convenient of the four operations and is the one most likely to be misused.

curl -X POST "https://api.ads.openai.com/v1/custom_audiences/merge" \
  -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: custom-audience-merge-001" \
  -d '{
    "name": "All qualified customers",
    "custom_audience_ids": ["caud_source_1", "caud_source_2"]
  }'

It combines 2 to 64 distinct, ready audiences in the same ad account, counting each matched user once. And then:

"The new audience is independent. The sources don't change, future source updates don't propagate to the merged audience, and existing campaigns don't automatically switch to the new ID."

A merged audience is therefore frozen at the moment you merged. If you build "all customers" by merging six regional lists and those lists are refreshed weekly, the merged audience decays from the first refresh onwards — silently, because nothing errors.

The maintainable pattern, given Replace exists, is usually to keep one audience and Replace its membership from a full snapshot you assemble yourself, rather than to merge six audiences you also have to maintain. Merge earns its place for genuinely one-off unions, not for standing audiences.

One more detail that is easy to miss: "Poll the operation using the custom_audience_id returned by Merge, not a source audience ID."

What this does not change

Worth stating plainly, because a changelog line invites over-reading:

  • Europe is still out. "Custom audiences are not supported for campaigns targeting the European Economic Area (EEA) or Switzerland, where personalized ads are not yet available." The new operations do not open these markets.
  • Exclusions still take precedence, and the remainder still has to clear the floor. If you include and exclude in the same campaign, "the remaining population must still meet the mi[nimum]". A small exclusion audience does not exempt the campaign from the inclusion minimum.
  • Don't include and exclude the same audience in one campaign.
  • Bid multipliers are unchanged, still expressed in micros — 1000000 is 1×, 2000000 is 2× — and "Small exclusion audiences aren't automatically eligible for bid adjustments."
  • Data rules are unchanged. First-party data only, no broker-sourced data, with the rights, notices and consents confirmed before upload under the Ad Tools Terms.

What we would change in a live account this week

Concrete, and scoped to what the documentation actually supports:

  1. Build the suppression list you previously could not. If your customer file is under 25,000, exclusion is now open to you. This is the single highest-value item in the update for small and mid-sized advertisers, and it costs one upload.
  2. Stop archiving audiences to refresh them. Move standing lists to Replace so campaign and ad-group references survive. Read membership_revision, submit it as expected_revision, poll the operation rather than the audience status.
  3. Audit your idempotency keys before you automate anything. A per-run key is correct; a per-audience or per-day key that gets reused across different payloads is a silent double-apply.
  4. Sequence audience jobs against campaign jobs. 409 custom_audience_mutation_conflict on a campaign edit is a documented outcome of running both at once.
  5. Replace ready-status checks with an intended_use query. Any code that gates on status == "ready" or on a matched count range is checking the wrong thing, by OpenAI's explicit instruction.
  6. Do not rewrite your Ads Manager runbook yet. Until the help centre is updated, the interface may still behave the way it documents. Change the API process; leave the manual process alone and note why.

A correction to our own earlier coverage

We published ChatGPT Ads Custom Audiences: The 25,000-Match Floor and the One-Way Door Nobody Mentions on 2026-08-18, and its central framing — that a custom audience is immutable once created — was accurate against the documentation available at the time and is no longer accurate for the API as of 2026-08-25. That page now carries a correction pointing here. We would rather flag our own stale page than leave it ranking on a fact OpenAI has since changed.

Sources

All captured 2026-08-26.

Last verified: 2026-08-26. OpenAI is documenting this product across two surfaces that currently disagree. We re-check both daily; if the help centre is brought into line, this page will say so.

Related Tools

Related Use Cases

Relevant Reads

One Audience Strategy, Every Channel

Soku pulls ChatGPT Ads into the same reporting surface as Google, Meta and TikTok — so an audience you can only maintain through an API does not become a result you can only guess at.

Get Started for Free

We use essential cookies to operate and secure Soku. With your permission, we also use optional analytics and advertising cookies to measure usage and campaigns. You can change your choice at any time. Privacy Policy