Class: AmazonAds::ApiClient
- Inherits:
-
Object
- Object
- AmazonAds::ApiClient
- Defined in:
- app/services/amazon_ads/api_client.rb
Overview
Faraday-backed client for the Amazon Advertising API (NA region) — the
profile + campaign listing surface used by AmazonAdsCampaignSyncWorker
to mirror campaigns into the sources table, the direct parallel to
Pinterest::AdvertiserApiClient / MicrosoftAds::CampaignManagementClient.
Amazon splits campaigns across per-ad-product endpoints with versioned
media types (Sponsored Products v3, Sponsored Brands v4, Sponsored
Display) and scopes every campaign call to a profile (one per
marketplace/account pair) via the Amazon-Advertising-API-Scope header.
#list_campaigns flattens all three ad products into one uniform list so
the sync worker doesn't care which product a campaign belongs to.
Authenticates with a lazily-refreshed OAuth access token from
OauthService plus the LWA client id header.
Defined Under Namespace
Classes: Request, Response, ThrottledError
Constant Summary collapse
- API_HOST =
NA-region API host — WarmlyYours advertises on amazon.com (+ .ca, .com.mx),
which all live under the NA endpoint. 'https://advertising-api.amazon.com'- PAGE_LIMIT =
Page size for the SP/SB list endpoints. Sponsored Brands v4 caps
maxResults at 100 (500 gets HTTP 400 INVALID_ARGUMENT / rangeError) —
the tightest limit of the list endpoints, so both use it; the cursor
loop absorbs accounts with more campaigns than one page. 100- DEFAULT_THROTTLE_DELAY =
Default delay exposed to a rescheduling worker when Amazon omits a
Retry-After header. The client never sleeps: holding a Sidekiq thread
cannot create quota, so callers persist their state and retry later. 20- MAX_THROTTLE_DELAY =
Ceiling for a server-supplied Retry-After. RFC 7231 permits an absolute
date; callers combine this bounded hint with their own durable cooldown
policy rather than accepting an arbitrarily distant retry time. 60- STATES =
Campaign states fetched from every ad product. ARCHIVED campaigns are
deliberately excluded — the sync worker archives any local Source whose
campaign drops out of this set. %w[ENABLED PAUSED].freeze
- REPORT_CONTENT_TYPE =
Media type every reporting call must send AND accept.
'application/vnd.createasyncreportrequest.v3+json'- REPORT_PRODUCTS =
adProduct→reportTypeIdfor campaign-grain reports. All three
accept the same column set (verified against the live API 2026-07-27). { 'SPONSORED_PRODUCTS' => 'spCampaigns', 'SPONSORED_BRANDS' => 'sbCampaigns', 'SPONSORED_DISPLAY' => 'sdCampaigns' }.freeze
- REPORT_COLUMNS =
Campaign-grain metric columns requested for every ad product.
costis
in the profile's own currency — callers spanning profiles must convert. %w[campaignId campaignName cost clicks impressions date].freeze
- CONVERSION_COLUMNS =
Conversions and attributed sales, named DIFFERENTLY per ad product:
Sponsored Products carries the attribution window in the column name,
Brands and Display do not. Verified against the live seller profile on
2026-08-02 — Amazon rejects the whole report for one unknown column, so
these are probed rather than inferred from the docs.7-day attribution for Sponsored Products: it matches what the
Advertising console and AdLabs report, which is what these numbers get
checked against. Widening to 14d/30d silently inflates every ROAS on the
dashboard, so change this only alongside whatever it is compared with.sales*is money in the profile's own currency, exactly likecost. { 'SPONSORED_PRODUCTS' => { purchases: 'purchases7d', sales: 'sales7d' }, 'SPONSORED_BRANDS' => { purchases: 'purchases', sales: 'sales' }, 'SPONSORED_DISPLAY' => { purchases: 'purchases', sales: 'sales' } }.freeze
Instance Method Summary collapse
-
#create_report(profile_id:, ad_product:, date:, end_date: nil) ⇒ String
Requests one daily campaign report.
-
#download_report(url) ⇒ Array<Hash>
Fetches and inflates a completed report.
-
#initialize(oauth: AmazonAds::OauthService.new) ⇒ ApiClient
constructor
A new instance of ApiClient.
-
#list_campaigns(profile_id:) ⇒ Array<Hash>
Every ENABLED or PAUSED campaign on the profile, across Sponsored Products, Sponsored Brands, and Sponsored Display, normalized to a uniform shape.
-
#profiles ⇒ Array<Hash>
All advertising profiles visible to the connected account — one per marketplace/account pair (seller US, vendor US, seller CA, …).
-
#report_status(profile_id:, report_id:) ⇒ Hash
status(PENDING|PROCESSING|COMPLETED|FAILED) and, once complete, a presignedurl.
Constructor Details
#initialize(oauth: AmazonAds::OauthService.new) ⇒ ApiClient
Returns a new instance of ApiClient.
69 70 71 |
# File 'app/services/amazon_ads/api_client.rb', line 69 def initialize(oauth: AmazonAds::OauthService.new) @oauth = oauth end |
Instance Method Details
#create_report(profile_id:, ad_product:, date:, end_date: nil) ⇒ String
Requests one daily campaign report.
Amazon deduplicates report requests by configuration + date range
(not by name) and answers a repeat with HTTP 425 carrying the original
report id. That is a feature, not an error: re-running a date reuses the
already-generating report, so this returns that id rather than raising.
timeUnit: DAILY means a multi-day range returns one row per campaign
per day, so a backfill wants ONE report for the whole range rather than
one per day — six requests instead of six-times-N, which is the
difference between working and being throttled into uselessness.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'app/services/amazon_ads/api_client.rb', line 151 def create_report(profile_id:, ad_product:, date:, end_date: nil) end_date ||= date body = { 'name' => "heatwave #{ad_product} #{date}..#{end_date}", 'startDate' => date.to_s, 'endDate' => end_date.to_s, 'configuration' => { 'adProduct' => ad_product, 'groupBy' => ['campaign'], 'columns' => REPORT_COLUMNS + CONVERSION_COLUMNS.fetch(ad_product).values, 'reportTypeId' => REPORT_PRODUCTS.fetch(ad_product), 'timeUnit' => 'DAILY', 'format' => 'GZIP_JSON' } } request(:post, '/reporting/reports', profile_id: profile_id, content_type: REPORT_CONTENT_TYPE, body: body)['reportId'] rescue RuntimeError => e duplicate_id = e.[/duplicate of\s*:?\s*([0-9a-f-]{36})/i, 1] raise unless duplicate_id duplicate_id end |
#download_report(url) ⇒ Array<Hash>
Fetches and inflates a completed report.
186 187 188 189 |
# File 'app/services/amazon_ads/api_client.rb', line 186 def download_report(url) raw = URI.parse(url).open(open_timeout: 15, read_timeout: 60, &:read) JSON.parse(Zlib::GzipReader.new(StringIO.new(raw)).read) end |
#list_campaigns(profile_id:) ⇒ Array<Hash>
Every ENABLED or PAUSED campaign on the profile, across Sponsored
Products, Sponsored Brands, and Sponsored Display, normalized to a
uniform shape.
89 90 91 |
# File 'app/services/amazon_ads/api_client.rb', line 89 def list_campaigns(profile_id:) sponsored_products(profile_id) + sponsored_brands(profile_id) + sponsored_display(profile_id) end |
#profiles ⇒ Array<Hash>
All advertising profiles visible to the connected account — one per
marketplace/account pair (seller US, vendor US, seller CA, …).
78 79 80 |
# File 'app/services/amazon_ads/api_client.rb', line 78 def profiles Array(request(:get, '/v2/profiles')) end |
#report_status(profile_id:, report_id:) ⇒ Hash
Returns status (PENDING|PROCESSING|COMPLETED|FAILED) and, once
complete, a presigned url.
176 177 178 179 |
# File 'app/services/amazon_ads/api_client.rb', line 176 def report_status(profile_id:, report_id:) request(:get, "/reporting/reports/#{report_id}", profile_id: profile_id, content_type: REPORT_CONTENT_TYPE) end |