OpenAI Ads Hardening Guide
Harden Ads Manager, ad-account access, OAuth and API keys, conversion measurement, partner integrations, customer audiences, feed ingestion, audit exports and signed lead delivery.
Overview
This is a product guide within the OpenAI platform. Ads Manager and the Advertiser API administer advertising accounts. They have separate roles, keys, billing, assets and integration permissions from API Platform projects and ChatGPT workspaces. OpenAI identifies Ads Manager as a beta product; enabled account features and billing arrangements determine which controls are available.
The source-first coverage ledger reconciles the full developer index, OpenAPI contract, account identity, browser measurement and connected services. This guide covers advertiser-controlled security boundaries. Campaign optimization, ad placement decisions and OpenAI-operated review systems are outside its hardening scope. Sponsored Agents are a selected-advertiser alpha, without a public advertiser configuration contract verified here; no invented agent policy is supplied.
Validation boundary: public first-party documentation and offline fixtures only. No live Ads account, credential, lead, campaign or configuration was used. Maturity remains AI-drafted; no product-specific CIS/STIG/SCuBA benchmark identifiers are asserted.
Intended Audience
- Ads account administrators and marketing operations teams
- Engineers connecting measurement, CRM, merchant and reporting systems
- Security and privacy reviewers assessing account access and customer-data flows
How to Use This Guide
Apply L1 (Crawl) identity, credential, consent, spend and evidence controls first. Use L2 (Walk) controls for automated audiences, feeds, external tools and lead delivery. A common identity-provider connection does not automatically grant access or enforce every Ads sign-in session.
Scope
Ads Manager and Advertiser API identity, credentials, account assets, delegated spend, measurement, feeds, partners, audit and lead delivery. Campaign performance advice and OpenAI-operated review systems are outside configurable hardening scope.
Table of Contents
- Identity and Credentials
- Spending and Delegated Actions
- Measurement and Connected Data
- Audit and Lead Delivery
- Compliance Quick Reference
1. Identity and Credentials
1.1 Review Ads Sign-In and Effective Account Access
Profile Level: L1 (Crawl)
Description
Configure Ads-specific identity and minimum account roles. A tenant identity role, Ads account-creation role and account membership are different grants. Direct and group access combine to the highest Ads account role. Preserve an effective account administrator during offboarding.
Rationale
Why This Matters:
- A narrow direct role cannot remove higher access inherited from a group.
- A successful OpenAI sign-in does not establish which advertiser the person should administer.
Attack Prevented: Unauthorized account administration and residual contractor access.
ClickOps Implementation
- In Admin Console → selected tenant → Global settings → Access, review the Ads sign-in policy. Follow the common identity controls for the actual connection and IdP MFA/conditional access. Test with SSO Optional where available and retain an authorized recovery path.
- Select the advertising account → Users & groups. Limit Admin, use Member for approved operators and Viewer for readers. Review direct and group roles. Tenant SCIM does not itself grant account access; assign synchronized groups to the specific account and review the initial Member role.
- Change synchronized group membership in the IdP. During removal, review every direct/group path and verify the resulting access while another administrator remains.
Session limitation: Ads documentation says an already signed-in person may access a Required-policy account without a new SSO sign-in. Test fresh and existing sessions; do not claim Required establishes continuous IdP enforcement. Off makes SSO unavailable. SSO setup
Automation: Unverified — the console identity paths are documented, but no Ads-specific SSO/SCIM/account-role write contract was verified in the current public Ads OpenAPI inventory. Obtain the supported tenant contract before automating; do not substitute API Platform project endpoints. Ads identity guide, checked 2026-11-05.
Validation & Testing
Test approved Viewer, Member and Admin behavior in the intended account, denial in a second account, and removal after every effective grant is withdrawn. Separately test fresh and existing sign-in sessions. Keep the reviewed access list and recovery owner.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-2, AC-3, AC-6, IA-2 |
1.2 Bind Keys and OAuth to the Intended Account
Profile Level: L1 (Crawl)
Description
Use the intended advertiser’s account-scoped Ads key, held server-side. Confirm the account before automation. The current OpenAPI contract also defines Ads OAuth: request read permission for reporting, add write only for approved changes, and request refresh/identity scopes only when needed. Model API keys, conversion keys and Ads OAuth are distinct contracts.
Rationale
Why This Matters:
- A valid credential for the wrong client can expose assets or change the wrong account.
- Long-lived delegated access can survive beyond the original operator’s task.
Attack Prevented: Cross-client changes, credential leakage and excessive partner access.
ClickOps Implementation
- Select the correct advertiser in Ads Manager → Settings → General → API Keys. Review each integration’s owner, purpose and approved secret storage. Follow the partner setup; keep each client’s credential separate.
- For an OAuth app, review
ads.admin.all.readversusads.admin.all.write, and whetheroffline_access,openidorprofileis necessary. The spec defines the authorization-code flow and/meapplication-scoped identity. Use the approved client registration and maintained OAuth library; this pass did not verify an Ads-specific public registration, refresh-rotation or revocation contract. - Revoke unnecessary access through the supported live account/app controls and test the result. Review cached partner tokens, scheduled jobs and exports when offboarding; replacing a human role alone is not proof that every integration is disabled.
Code Implementation
Code Pack: API Script
account=$(ads_get '/ad_account')
jq -e --arg expected "$OPENAI_ADS_EXPECTED_ACCOUNT_ID" \
'(.id | type == "string") and .id == $expected' <<<"$account" >/dev/null || {
echo 'Credential belongs to an unexpected or unreadable Ads account' >&2; exit 1;
}
jq '{id, status, currency_code, timezone, review_status: .review.status,
integrity_review_status: .account_integrity_review.review.status}' <<<"$account"
# A missing review is unknown, not an approval. This does not assess roles/SSO.
This GET fails if the returned account ID differs from an independently approved value. It creates no key and proves neither OAuth consent nor effective account roles. The audit endpoint rejects Ads OAuth access tokens; use a reviewed account key for that exporter.
Validation & Testing
Check account ID, currency, timezone and review state before enabling a workflow. Test an unexpected-account fixture and require failure. Keep secret values, returned key-creation responses and bearer headers out of logs. Preserve the previous approved integration until replacement access is verified, then remove it deliberately.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-6, IA-5, SC-12 |
2. Spending and Delegated Actions
2.1 Enforce Reviewed Spend and Activation Boundaries
Profile Level: L1 (Crawl)
Description
Use available account spending limits as a boundary against runaway or compromised campaigns. Current daily/date-range limits require eligible postpaid invoice billing and billing-management permission, including reads. Campaign budgets remain separate. Review account, campaign, ad-group and ad activation deliberately.
Rationale
Why This Matters:
- A compromised writer can spend through several campaigns.
- Retrying a conflicted configuration blindly can defeat another administrator’s change.
Attack Prevented: Unbounded advertising spend and accidental activation of unreviewed assets.
ClickOps Implementation
- In the selected account, review available account spending controls and campaign budgets. Where the interface does not expose the required field, use the documented native API; no precise spend-settings menu is asserted by the fetched developer guide.
- Read the current limit windows, account currency/timezone, permitted dates and configuration revision. Choose a daily or date-range limit consistent with approved interruption tolerance. Amounts are integer micros of account currency, distinct from API Platform USD cents.
- For a daily edit, submit the latest
expected_revision; refresh and reconcile after 409. After 503, read back before retrying because the write may have succeeded. New daily limits start tomorrow or later; they do not provide an immediate incident stop. - Use the documented account/campaign pause path for incident containment, then verify actual state. Do not use irreversible archive as a temporary pause. Brand-name changes can pause delivery for review; missing review data is unknown, and active account status alone does not prove readiness. Account lifecycle
Code Implementation
Code Pack: Config
jq -n --argjson amount "$ADS_DAILY_AMOUNT_MICROS" --argjson revision "$ADS_EXPECTED_REVISION" \
'if $amount <= 1000000000000000 then
{amount_micros:$amount, expected_revision:$revision}
else error("Amount exceeds documented maximum") end'
# Native body for POST /ad_account/daily_spend_limit (edit an existing rule).
# Creation/recreation also needs a reviewed start_date from current allowed dates.
# Refresh revision on conflict; never silently retry with a new revision.
Code Pack: API Script
ads_get '/ad_account/spend_limit_windows' | jq -e '
if (.data|type == "array") and (.revision == null or (.revision|type == "number"))
then {revision, earliest_daily_start_date, can_create_daily_limit, daily_limit,
windows: [.data[] | {window_id, start_date, end_date, amount_micros, status,
can_edit, can_delete, spent_micros}]}
else error("Unexpected spending-limit response") end'
# Preserve unknown/missing counters as null. evaluated_at is not spend freshness.
The read assessment and native daily-edit payload emitter make no configuration writes. They do not enroll an ineligible account or bypass billing permissions.
Validation & Testing
Read back the saved configuration and retained spend counter, and check the chosen timezone and exclusive end date. Limits cover billable ad spend rather than taxes/invoices; stop propagation and delayed billing can permit overrun. Do not lower a cap or pause production merely to test it. An approved rollback must restore reviewed values with the latest revision.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-6, CM-3, SC-5 |
2.2 Review Plugin Actions and External App Authorization
Profile Level: L2 (Walk)
Description
Gate applications that can access Ads data and review proposed AI-assisted changes. For Business/Enterprise organizations, HubSpot Ads access defaults off and requires a global admin’s approval; organization approval remains separate from a user’s Ads authorization. The Ads Manager plugin scopes access to the connected user’s accessible resources and requires confirmation for supported writes.
Rationale
Why This Matters:
- An identity-only sign-in does not authorize an external app to read advertiser data.
- Natural-language requests can select the wrong account or interpret untrusted creative content as instructions.
Attack Prevented: Unauthorized delegation and unintended campaign, asset or account-access changes.
ClickOps Implementation
- A global admin opens Admin Console → External access → organization owning the Ads account → application → Access. Allow only approved Ads permissions and review connected users; disable obsolete app access. Tenant administration
- Review each plugin change’s selected account, target resource, current/proposed values, destination and budget before confirming. Recheck any revised proposal and compare the post-change read-back with the approval record.
- Keep recurring plugin reviews recommendation-only. They do not automatically optimize campaigns. Verify billing, legal identity and account readiness in Ads Manager; the plugin cannot override those checks.
Automation: Unverified — documented console/plugin paths were checked 2026-11-05, but no current Ads-specific external-access policy-write schema was verified. Obtain the supported policy handle before automating; API campaign write permission is not a tenant app-approval API. External access
Validation & Testing
Test denied app access and an approved reader separately. Review whether plugin write proposals require the intended confirmation and reapproval when values change. Use draft/preview operations before approved changes and audit the resulting account state.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-3, AC-6, CM-3, SA-9 |
3. Measurement and Connected Data
3.1 Gate Browser Measurement and Minimize Signals
Profile Level: L1 (Crawl)
Description
When measurement requires consent, set the official Pixel’s consent to false before initialization, then connect grant/withdrawal to the site’s consent system. Its documented default is true unless explicitly denied or a stored denial exists. Review optional matching fields, automatic advanced matching and every Pixel recipient.
Rationale
Why This Matters:
- Initialization before consent can transmit events immediately.
- Hashing identifiers does not make unlimited collection or sharing appropriate.
Attack Prevented: Unapproved customer-data disclosure and unintended cross-advertiser event routing.
ClickOps Implementation
- In Ads Manager → Conversions, confirm the intended data source/Pixel. Inventory tag-manager installations, Shopify integration and custom code; avoid duplicate recipients and production debug logging.
- In the website’s supported deployment/tag system, put
consent(false)beforeinit, then wire actual consent changes. Declining consent blocks measurement pings and removes Pixel cookies; newly granted consent permits future events without replaying previously blocked events. - Review optional user data and automatic advanced matching with the privacy owner. AAM detects supported information on the site and hashes it in the browser. Apply the required notices/permissions and stop server-side forwarding after withdrawal too. Image tags must render only after required consent; they do not inherit a separate JavaScript queue’s protection. Image tag
- Merge only the documented CDN/event hosts into CSP, using the site’s nonce/hash mechanism. Do not add
unsafe-inlinefor the Pixel. Specify the intended Pixel ID when using multiple pixels, and review all destinations. CSP and multiple pixels
Code Implementation
Code Pack: SDK Script
export function initializeConsentControlledPixel(oaiq, pixelId) {
if (typeof oaiq !== "function" || typeof pixelId !== "string" || !pixelId.trim()) {
throw new Error("Load the official Pixel queue and choose the approved Pixel ID");
}
// Queue this BEFORE init; the vendor default is consent=true.
oaiq("consent", false);
oaiq("init", { pixelId, debug: false });
return {
grant: () => oaiq("consent", true),
withdraw: () => oaiq("consent", false),
};
}
// Connect grant/withdraw to actual consent state before emitting events.
// Consent is not a per-Pixel isolation mechanism. For multiple pixels, review
// every recipient and keep server-side forwarding consistent with withdrawal.
Validation & Testing
In a disposable browser test, check the network before consent, after grant and after withdrawal. Check cookies and every configured Pixel, including duplicate tags and non-JavaScript image tags. Cookie duration is not a server-retention policy. Keep test identifiers synthetic and do not infer historical deletion from withdrawal.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-4, PT-2, PT-3, SC-7 |
3.2 Protect Server Conversion Credentials and Delivery
Profile Level: L1 (Crawl)
Description
Send Conversions API events from the server using a Conversions key and intended Web data-source Pixel ID. Keep raw identifiers and keys out of browser bundles, URLs, logs and support messages. Use the same event ID and Pixel ID for the same browser/server conversion so retries do not inflate results.
Rationale
Why This Matters:
- A leaked conversion key permits forged event traffic.
- Repeated or mismapped events can poison optimization and disclosure records.
Attack Prevented: Credential theft, conversion-data poisoning and excessive customer-data transfer.
ClickOps Implementation
- In Conversions, identify the approved Pixel and manage conversion credentials through the supported account flow. Conversion setup is gated for some accounts; a 404/403 is not permission to reuse an API Platform key.
- Define the minimum approved events and matching fields. Follow that endpoint’s exact normalization/hashing rules; Pixel and custom-audience phone normalization differ. Stop forwarding revoked-consent signals, including the browser reference.
- Validate synthetic batches with
validate_only: truebefore production. Validation transmits data to OpenAI even though events are not saved; it is not an offline or zero-retention promise. One invalid event rejects the full batch, up to 1,000 events. - Preserve stable event IDs through retries and validate the correct data source.
integration_sourceidentifies the sender but does not authenticate it. Treat recent raw-event diagnostics as sensitive and limited, not a historical audit archive.
Code Implementation
Code Pack: API Script
body=$(jq -ce 'if (.events|type == "array") and (.events|length > 0 and length <= 1000)
then . + {validate_only:true} else error("Supply 1–1000 approved fixture events") end')
if [[ "${1:-}" != '--send' ]]; then printf '%s\n' "$body"; exit 0; fi
: "${OPENAI_ADS_CONVERSION_KEY:?Inject a server-side Conversions API key}"
: "${OPENAI_ADS_PIXEL_ID:?Set the approved Web data-source Pixel ID}"
[[ "$OPENAI_ADS_PIXEL_ID" =~ ^[A-Za-z0-9_-]+$ ]] || exit 2
base=$(ads_checked_base "${OPENAI_ADS_CONVERSION_BASE_URL:-https://bzr.openai.com}" 'https://bzr.openai.com')
umask 077
work=$(mktemp -d); trap 'rm -rf "$work"' EXIT
trap 'exit 1' HUP INT TERM
printf '%s' "$body" > "$work/request"
(( $(wc -c < "$work/request") <= 8388608 )) || { echo 'Conversion request exceeds 8 MiB safety bound' >&2; exit 2; }
status=$(ads_http_to_file "$base" "$OPENAI_ADS_CONVERSION_KEY" \
"/v1/events?pid=${OPENAI_ADS_PIXEL_ID}" "$work/response" \
--request POST -H 'Content-Type: application/json' --data-binary "@$work/request")
[[ "$status" == 200 ]] || { echo "Conversion validation failed: HTTP $status; response body withheld" >&2; exit 1; }
cat "$work/response"
# Never change validate_only to false as part of a retry. Browser and server
# senders must retain the same event id AND Pixel ID for the same conversion.
The default emits a native validation request only. --send is an explicit data transfer using injected credentials; this run did not use it against OpenAI.
Validation & Testing
Check keys stay in the server secret manager and that generated requests force validation mode. Confirm the same synthetic conversion retains its ID across senders/retries. Test withdrawal on both paths, batch rejection and an incorrect Pixel mapping before rollout. Do not log complete request/response bodies containing customer signals.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | IA-5, PT-2, SC-8, SI-10 |
3.3 Limit Customer Audiences and Reconcile Membership
Profile Level: L2 (Walk)
Description
Upload only approved first-party customer audiences, minimize identifiers, and preserve the requested removal/replacement lifecycle. Broker-sourced lists are disallowed by the current guide. Hashing reduces raw identifier transfer but does not supply permission, consent or anonymity.
Rationale
Why This Matters:
- A customer list is a separate sensitive data store and access grant.
- An accepted asynchronous update can be incomplete or partially applied.
Attack Prevented: Unauthorized audience disclosure and stale inclusion after an intended removal.
ClickOps Implementation
- Use the intended account’s custom-audience workflow and an approved source list. Retain the permitted purpose, owner and removal process. Follow Ads audience guidance, including current regional/targeting restrictions.
- Prefer prehashed identifiers where appropriate; normalize email by trimming/lowercasing while preserving plus tags and dots. Audience phone hashes use E.164 with the leading plus; do not reuse Pixel phone hashes.
- For Add/Remove/Replace/Merge, save the original body, idempotency key, account, audience and operation IDs. Poll the returned operation to completion. A ready audience does not prove a replacement completed, and a merge is an independent list whose future source changes do not propagate.
- Reconcile revision conflicts and partially applied Add/Remove operations by operation ID; do not start duplicate work. Cancel only before the supported operation begins changing membership. Review dependent campaigns before archiving an audience.
Code Implementation
Code Pack: Config
emails=$(jq -er 'if type == "array" and all(.[]; type == "string" and
test("^[[:space:]]*[A-Za-z0-9.!#$%&\u0027*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+[[:space:]]*$"))
then .[] | gsub("^[[:space:]]+|[[:space:]]+$"; "") | ascii_downcase
else error("Expected approved ASCII emails; handle international identifiers separately") end')
printf 'email_sha256\n'
while IFS= read -r email; do
[[ -n "$email" ]] || continue
printf '%s' "$email" | shasum -a 256 | cut -d ' ' -f 1
done <<<"$emails"
# Preserve email plus tags and dots. Hash UTF-8 without a trailing newline.
# Phone normalization differs between custom audiences and Pixel matching;
# never reuse a Pixel phone hash for the E.164 audience contract.
The native CSV emitter handles approved ASCII email inputs only, without uploading them. International identifiers and phone/GAID formats require their own documented normalization path.
Validation & Testing
Check hashes against known synthetic fixtures and avoid newline hashing. Require successful operation status and the new revision before dependent changes. Review independent merged audiences and retained source/export files during offboarding. Use privacy-safe counts rather than attempting to recover individual membership outcomes.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-4, PT-2, SI-12 |
3.4 Protect Feed Ingestion and Catalog Lifecycle
Profile Level: L2 (Walk)
Description
Restrict product/hotel feed ingestion credentials and review catalog replacement, destinations and serving eligibility. SFTP upload access and campaign delivery are different controls. Public product/image URLs must not contain customer secrets or private catalog data.
Rationale
Why This Matters:
- A compromised feed writer can replace destinations or prices at scale.
- Pausing ingestion leaves existing ads and catalog objects in place.
Attack Prevented: Catalog tampering, malicious landing-page substitution and incomplete offboarding.
ClickOps Implementation
- In Ads Manager → Tools → Feeds, review the feed, import source and upload history. UI import options include file, hosted HTTPS and SFTP. Restrict source access and review destination domains, image URLs and ads/search/checkout eligibility deliberately.
- For API SFTP workflows, select documented SSH-key authentication where practical, keep the private key outside the platform and rotate by reviewed replacement. Read back enabled/authentication state; do not export connection credentials into inventory logs.
- Review
is_full_refreshfor eligible catalogs: full uploads can delete missing products within a day. Disabling it stops pending cleanup but does not restore deleted products. Delta updates do not constitute a full refresh. The current prose documents this field while the fetched OpenAPI settings schema omits it; verify availability before relying on it. - To offboard, pause the upload connection and the relevant delivery resources. Feed archive requires dependent campaigns/ad groups to be archived; pausing alone is insufficient. Preserve approved evidence and recovery copies before irreversible archive. Hotel inventory is limited beta with separate format/availability requirements; apply the same credential and data-boundary review. Hotel guide
Code Implementation
Code Pack: API Script
ads_get "/feeds/${OPENAI_ADS_FEED_ID}/sftp_access" |
jq -e 'if (.enabled|type == "boolean") then {enabled, authentication_method}
else error("Unexpected SFTP access response") end'
ads_get "/feeds/${OPENAI_ADS_FEED_ID}/settings" |
jq '{id, is_eligible_search, is_full_refresh}'
# A missing is_full_refresh is unknown: current prose documents an eligible
# feature that is not yet listed in this OpenAPI settings schema.
Validation & Testing
Confirm the reader exposes no SFTP URI/password. Use synthetic catalog data to validate accepted versus processed status and intended product eligibility. Missing fields are unknown, not false. Test replacement/deletion semantics in a non-production feed; retain a reviewed restoration plan.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-6, CM-3, SI-7, SI-12 |
3.5 Review Each Connected Data Source and Disconnect Boundary
Profile Level: L2 (Walk)
Description
Treat GA4, Shopify, HubSpot and measurement partners as separate data processors and credential recipients. Verify the actual account, property/Pixel, event mappings and retained copies. Shared ownership and successful connection do not establish minimum disclosure.
Rationale
Why This Matters:
- Disconnection may stop new transfers while preserving imported data and active campaigns.
- A broad analytics source can disclose activity beyond OpenAI-attributed conversions.
Attack Prevented: Wrong-property disclosure, excessive event forwarding and lingering partner access.
ClickOps Implementation
- GA4 beta: in Conversions → Data Sources → Create → Google Analytics 4, review organization authorization, read-only Google access and the selected property ID. Inspect mappings and last sync. Imports include property-wide aggregate activity across sources. Disconnect through the property’s menu to stop new imports; previously imported reports remain. Respect Google Workspace restrictions. GA4 guide
- Shopify: open the ChatGPT Ads app → Connect Account, verify the advertiser and Pixel, and review inventory/commerce-event sharing. On disconnect, review existing campaigns/feed dependencies separately; the Ads account and campaigns are not deleted. Shopify guide
- HubSpot: use Settings → Tools → Marketing → Ads → Connect account → ChatGPT Ads after organization app approval in 2.2. Review requested permissions and lead/CRM workflows. Disconnect through Settings → Integrations → Connected Apps → ChatGPT Ads → Actions → Uninstall, then assess retained leads and existing campaigns separately. HubSpot guide
- Measurement/mobile partners: choose the credential type the integration requires, use the intended Web Pixel and conversion key where specified, and allow only approved event fields, postbacks and attribution sources. Review the partner’s access and storage separately. Mobile partner settings and attribution windows live in that partner’s platform. Measurement partners, mobile partners
Automation: Unverified — the connection/disconnect console paths were checked 2026-11-05, but a universal Ads partner-connection contract and partner-specific APIs were not verified. Existing account, feed, audience and lead read packs assess only their named objects. Use the current supported partner contract before automating these connections.
Validation & Testing
Record the approved recipient/account/property/Pixel and event list. Test synthetic events and compare source activity with imported data. For offboarding, stop transfers, disable app permissions and credentials as appropriate, inspect campaigns and coordinate retention/deletion with each processor. Disconnect is not a deletion attestation.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-4, PT-2, SA-9, SI-12 |
4. Audit and Lead Delivery
4.1 Export Account Audit Records with Stable Cursors
Profile Level: L1 (Crawl)
Description
Export the selected account’s Ads audit logs into a protected sink. Authenticate this endpoint with an Ads account key, not OAuth. Use audit_log_type, account audit IDs and the documented first_id/last_id cursor semantics; API Platform event schemas do not apply.
Rationale
Why This Matters:
- Console-only change history is difficult to correlate with partner activity.
- Advancing a checkpoint after a partial export loses evidence silently.
Attack Prevented: Undetected campaign changes and evidence gaps during an account incident.
ClickOps Implementation
- Select the intended advertiser and review its supported change logs; the Ads Manager overview identifies account change-history capability. Use native API export for repeatable collection.
- Export a reviewed time window with stable filters/order and up to 100 records per page. Follow
last_idasafter, rather than resource IDs;actor_idfilters changes made by that actor. - Stage output, require successful completion and then advance a durable checkpoint. Overlap the next window and deduplicate on audit-log ID. Protect actor information and changed values, and map the actual schema at ingestion.
Code Implementation
Code Pack: API Script
ads_audit_list
# Stage stdout and commit only on exit 0. Overlap the next window, deduplicate
# on audit-log id and retain the same order/filters across cursor pages.
# Export may contain personal identities and changed values: protect the sink.
Code Pack: Sigma Detection Rule
detection:
selection:
audit_log_type: update_campaign
condition: selection
falsepositives:
- Approved campaign edits; correlate actor and changed fields with the change record.
level: medium
The experimental Sigma rule uses the documented update_campaign example. It is a review signal rather than proof of compromise; broader event coverage must use observed/documented values.
Validation & Testing
Test multiple pages, repeated/missing cursors, HTTP errors and unexpected response shapes against fixtures. Require a nonzero exit on incomplete collection and verify the last page in the sink. Replay a synthetic campaign change through actual ingestion before treating the detection as operational.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AU-2, AU-6, AU-9, AU-12 |
4.2 Bind Signed Lead Delivery to Its Subscription
Profile Level: L2 (Walk)
Description
For available lead-sync integrations, bind each approved HTTPS receiver and signing secret to its account and subscription generation. The current Ads OpenAPI webhook contract defines signed lead_form.response.created delivery, freshness headers and durable acknowledgement. A valid signature alone does not authorize arbitrary CRM routing or contact use.
Rationale
Why This Matters:
- An unsigned routing header can select the wrong customer context.
- A retried or synthetic callback can create duplicate or fictitious CRM contacts.
Attack Prevented: Forged leads, cross-account routing, replay and duplicate processing.
ClickOps Implementation
The public contract supplies API operations rather than an Ads Manager setup menu for custom lead-sync subscriptions. Use the native API and your approved receiver deployment process; no console toggle is asserted.
- Review the destination, account and consent/retention purpose before creating a subscription. Native creation requires an idempotency key and returns a signing secret; store it directly in the approved secret manager. This pack does not create it or print it.
- Use
OpenAI-Subscription-Idonly to select a preprovisioned candidate key, verify the raw body with Standard Webhooks v1 HMAC-SHA256 and timestamp checks, then require its exact match with signedsubscription_id. Verify the signed account matches the registry. - Validate the full published event/response schema, minimize retained lead fields and isolate signed
syntheticprobes from production CRM processing. Atomically enqueue and deduplicate by subscription generation plus delivery ID before 2xx; retry storage failures. - For offboarding, inventory subscriptions, delete the approved native subscription and stop downstream forwarding. Coordinate key retirement, pending deliveries and CRM retention separately; deletion of a subscription is not historical lead deletion.
Code Implementation
Code Pack: API Script
ads_get '/lead_sync_subscriptions' \
--data-urlencode "ad_account_id=${OPENAI_ADS_EXPECTED_ACCOUNT_ID}" |
jq -e 'if (.data|type == "array") and .has_more == false
then [.data[] | {subscription_id, status}]
else error("Unexpected or incomplete lead subscription response; reconcile before use") end'
# The current GET schema has no cursor request parameter. Refuse has_more=true
# rather than inventing pagination or declaring a partial list complete.
Code Pack: SDK Script
export function createLeadReceiver({ findSubscription, validateLeadEvent, enqueueOnce }) {
if ([findSubscription, validateLeadEvent, enqueueOnce].some((fn) => typeof fn !== "function")) {
throw new Error("Supply subscription lookup, full published-schema validation and durable enqueue adapters");
}
return async function receive(request) {
if (request.method !== "POST") return new Response(null, { status: 405 });
const candidateId = request.headers.get("OpenAI-Subscription-Id");
if (!candidateId || !/^leadsync_[^~]+_g[1-9][0-9]*$/.test(candidateId) ||
["webhook-id", "webhook-timestamp", "webhook-signature"].some((name) => !request.headers.get(name))) {
return new Response(null, { status: 400 });
}
// Header is untrusted: use it only to look up a preprovisioned candidate.
// Never construct a URL or fetch a secret based on an arbitrary header.
const subscription = await findSubscription(candidateId);
if (!subscription) return new Response(null, { status: 400 });
let event;
try {
event = await subscription.client.webhooks.unwrap(await request.text(), request.headers);
} catch (error) {
if (error instanceof OpenAI.InvalidWebhookSignatureError || error instanceof SyntaxError) {
return new Response(null, { status: 400 });
}
throw error;
}
if (!validateLeadEvent(event) || event.type !== "lead_form.response.created" ||
event.subscription_id !== candidateId) return new Response(null, { status: 400 });
// Signed synthetic probes must never become CRM contacts.
if (event.synthetic === true) return new Response(null, { status: 204 });
if (event.data.ad_account_id !== subscription.adAccountId) return new Response(null, { status: 400 });
try {
// Store a minimized, validated event and delivery id in ONE durable
// transaction. Scope the unique key to the subscription generation.
await enqueueOnce(`${candidateId}:${request.headers.get("webhook-id")}`, event);
} catch {
return new Response(null, { status: 503 });
}
return new Response(null, { status: 204 });
};
}
// validateLeadEvent must enforce LeadSyncWebhookEvent and referenced data/response
// schemas from the current published Ads OpenAPI contract. Bind each registry
// entry to an official OpenAI client configured with that signing secret.
// Never log raw lead responses or treat a valid signature as consent for any use.
The receiver requires a full-schema validator, registry and durable queue adapters. Its official SDK verifies the standard signature; it does not start a server. The native list has no documented cursor input in this spec, so the assessment refuses has_more: true instead of claiming completeness.
SDK examples use the isolated packs/openai-ads/sdk package and committed Bun lockfile. Install with bun install --cwd packs/openai-ads/sdk --frozen-lockfile --ignore-scripts; the receiver and offline fixtures use its official openai 7.51.0 dependency.
Validation & Testing
Use signed synthetic fixtures to test tampering, stale timestamps, subscription mismatch, wrong account, unknown key, duplicate delivery and unavailable storage. Test production events only in the authorized environment and confirm probes never enter CRM. Do not log raw answers, signing secrets or authentication headers.
Compliance Mappings
| Framework | Mapping |
|---|---|
| NIST 800-53 Rev 5 | AC-4, SC-8, SI-10, SI-12 |
5. Compliance Quick Reference
| Area | Controls | Example NIST 800-53 Rev 5 families |
|---|---|---|
| Identity and delegated access | 1.1–1.2, 2.2 | AC, IA, SA |
| Financial and asset integrity | 2.1, 3.4 | CM, SC, SI |
| Customer-data boundaries | 3.1–3.5, 4.2 | AC, PT, SA, SI |
| Evidence and callback integrity | 4.1–4.2 | AU, SC, SI |
Appendix A: Implementation and Source Boundaries
The official API Platform Terraform provider’s full resources/data-source lists contain no Ads account resources in the fetched 1.5.0 inventory. The first-party OpenAI CLI and SDK API-administration census does not supply an Ads account CLI namespace. This guide uses verified Ads REST, the official browser Pixel/SDK verification primitives, native payload/file emitters and an experimental audit detection; it does not invent Terraform, GraphQL, MDM or SQL surfaces.
REST helpers accept only the fixed Ads or Conversions API base, put credential headers in curl stdin configuration, ignore curlrc, follow no redirects, and bound each response to 8 MiB/30 seconds with a 5-second connection timeout. Audit pages are staged in private temporary files and emitted only after full success, with a 64 MiB/10,000-page ceiling. Narrow export windows after a bound failure; keep checkpoints unchanged. Only explicit HTH_FIXTURE_LOOPBACK=1 permits 127.0.0.1 fixture bases. Conversion validation also stages its request body privately so neither credentials nor event bodies appear in curl arguments.
Ads OAuth registration/refresh/revocation details, partner-specific APIs and Sponsored Agent configuration remain research/availability limitations, not absent capabilities. The source ledger records them. Public implementation evidence and offline mocks do not prove console paths, account entitlement, consent compliance, receiver deployment or actual enforcement.
References
- Ads full documentation and OpenAPI
- Ads identity and access
- API authentication and audit logs
- Measurement Pixel, Conversions API, custom audiences and feeds
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-11-05 | 0.1.0 | ai-drafted | Initial source-first Ads guide: account identity, scoped keys/OAuth, financial/action boundaries, consent and conversion data, audiences/feeds/partners, audit and signed lead delivery. Public sources and offline tests only; no tenant exercised. | Codex (GPT-6) |