Class: OpenaiAds::AdvertiserApiClient

Inherits:
BaseService show all
Defined in:
app/services/openai_ads/advertiser_api_client.rb

Overview

Service object: advertiser API client.

Constant Summary collapse

BASE_URL =

URL for base.

'https://api.ads.openai.com/v1'
PAGE_LIMIT =

Page size for the campaigns list endpoint. The API caps at 500,
which is well above the realistic active-campaign count for one
ad account (Google Ads parallel runs ~50 campaigns) so we expect
a single page in the common case.

500

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#campaign_insights(token:, campaign_id:) ⇒ Array<Hash>

Daily performance rows for ONE campaign.

Verified against the live API 2026-07-27 — three traps, all load-bearing:

  • insights are per campaign (/campaigns/{id}/insights); there is
    no account-level or flat /insights endpoint (they 404).
  • fields must use indexed query syntax (fields[0]=…) and the
    dotted vocabulary; a comma string or fields[] is a 400.
  • start_date/end_date are accepted and ignored — the endpoint
    always returns its full retained window, so callers MUST filter on
    readable_time themselves.
    spend is in whole currency units (dollars), NOT micros, and there is no
    conversions field in the vocabulary.

Parameters:

  • token (String)

    advertiser API key

  • campaign_id (String)

Returns:

  • (Array<Hash>)

    rows keyed campaign_id, spend, clicks,
    impressions, readable_time (YYYY-MM-DD)

Raises:

  • (Faraday::Error)

    on persistent transport errors



120
121
122
123
124
125
126
127
# File 'app/services/openai_ads/advertiser_api_client.rb', line 120

def campaign_insights(token:, campaign_id:)
  fields = %w[campaign.id campaign.spend campaign.clicks campaign.impressions metadata.readable_time]
  query = fields.each_with_index.map { |f, i| "fields[#{i}]=#{f}" }.join('&')

  response = connection(token).get("campaigns/#{campaign_id}/insights?#{query}")
  raise_unless_ok(response, 'campaign_insights')
  Array(safe_parse(response.body)['data'])
end

#create_paused_ocpc_product_feed_campaign(token:, name:, description:, product_feed_id:, lifetime_spend_limit_micros:, conversion_event_setting_id:, targeting:) ⇒ Hash

Create the shell for a product-feed oCPC experiment. This deliberately
hardcodes paused so an account-management script cannot accidentally
begin spending before the ad group, CPA bid, product set, and ad template
have been reviewed. OpenAI requires exactly one standard conversion event
for conversion bidding; accepting one ID instead of an array preserves
that invariant at the client boundary.

Parameters:

  • token (String)

    advertiser API key

  • name (String)

    campaign name

  • description (String)

    campaign description

  • product_feed_id (String)

    linked product-feed ID

  • lifetime_spend_limit_micros (Integer)

    lifetime budget in micros

  • conversion_event_setting_id (String)

    active standard conversion event ID

  • targeting (Hash)

    explicit location targeting copied from a reviewed campaign

Returns:

  • (Hash)

    the created, paused campaign



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/services/openai_ads/advertiser_api_client.rb', line 81

def create_paused_ocpc_product_feed_campaign(token:, name:, description:, product_feed_id:,
                                             lifetime_spend_limit_micros:, conversion_event_setting_id:,
                                             targeting:)
  payload = {
    name:,
    description:,
    status: 'paused',
    mode: 'product_feed',
    product_feed_id:,
    budget: { lifetime_spend_limit_micros: },
    bidding_type: 'conversions',
    conversion_event_setting_ids: [conversion_event_setting_id],
    targeting:
  }

  response = connection(token).post('campaigns') { |req| req.body = payload.to_json }
  raise_unless_ok(response, 'create_paused_ocpc_product_feed_campaign')
  body = safe_parse(response.body)
  body['data'] || body
end

#list_campaigns(token:) ⇒ Array<Hash>

List all campaigns on the ad account. Walks the cursor pagination
until has_more is false and returns the concatenated list.

Parameters:

  • token (String)

    OpenAI Ads API key (bearer).

Returns:

  • (Array<Hash>)

    campaign objects per OpenAI's spec —
    id, name, status ("active"|"paused"|"archived"),
    created_at/updated_at (Unix timestamps), start_time/
    end_time, budget, targeting, etc.

Raises:

  • (Faraday::Error)

    on persistent transport errors after the
    normal Faraday retry budget.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'app/services/openai_ads/advertiser_api_client.rb', line 41

def list_campaigns(token:)
  campaigns = []
  cursor    = nil

  loop do
    response = connection(token).get('campaigns') do |req|
      req.params['limit'] = PAGE_LIMIT
      req.params['after'] = cursor if cursor
      req.params['order'] = 'asc'
    end

    raise_unless_ok(response, 'list_campaigns')
    body = safe_parse(response.body)
    page = Array(body['data'])
    campaigns.concat(page)

    break unless body['has_more']

    cursor = body['last_id'] || page.last&.dig('id')
    break unless cursor # defensive — `has_more` true with no cursor would loop forever
  end

  campaigns
end