Home / Blog / Amazon Product API in Ruby: Net::HTTP Client, Retries and Threads

Amazon Product API in Ruby: Net::HTTP Client, Retries and Threads

Get Amazon product data in Ruby with nothing but the standard library: a Net::HTTP client for product lookup and keyword search, a retry policy that never pays twice for the same miss, a thread pool for bulk lookups, and how to call it from Rails.

September 24, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Blueprint flow from a Ruby script through Net::HTTP to the SellerMagnet API and back as parsed JSON

To get Amazon product data in Ruby, send one GET request with Net::HTTP to /api/amazon-product-lookup with an ASIN, a marketplace ID and your API key in the X-Api-Key header, then JSON.parse the body. No gem is needed. This quickstart builds a small client for lookups and keyword search, adds a retry policy that never pays twice for the same miss, runs bulk lookups on threads, and shows where the client belongs in a Rails app. Older Ruby gems wrap Amazon's Product Advertising API, which is retired - see the PA-API alternative guide.

Key Takeaways

  • The standard library is enough: Net::HTTP, JSON, URI, Queue and Thread. The code runs on Ruby 2.6 and later.
  • Set read_timeout to 120 seconds; the API allows a scrape that long, and a request you abandon is still billed.
  • Retry 429, 500, 502 and 503 only. A 404 means the ASIN is not on that marketplace; retrying it buys the same answer again.
  • Net::HTTP is not thread-safe and re-sends a timed-out GET by default: one client per thread, and max_retries = 0.
  • In Rails, call the API from a background job, never inside a web request that could wait up to 120 seconds.

What do I need before I start?

  • Ruby 2.6 or later - the version macOS ships is enough.
  • An API key - a free account includes 150 credits, one per request.
  • The key in an environment variable, or in Rails credentials, so it never lands in your repository.

Check Ruby and put the key in the environment

ruby -v                        # ruby 2.6 or later
export SELLERMAGNET_API_KEY="your-key-here"
ruby seller_magnet.rb          # one lookup, one credit

How do I get Amazon product data in Ruby?

Wrap one keep-alive Net::HTTP connection in a small client class. get raises a single ApiError carrying the status, the API's own message and any Retry-After, so callers only ever see data or that exception. The lookup method picks the fields most applications need; max_retries = 0 matters: by default Net::HTTP silently re-sends a GET that timed out, and every send is billed. marketplaceId takes one of 23 marketplace IDs, such as ATVPDKIKX0DER for amazon.com.

seller_magnet.rb - a standard-library client with one exception type

require "json"
require "net/http"
require "openssl"
require "uri"

module SellerMagnet
  API = URI("https://sellermagnet-api.com/api/")

  class ApiError < StandardError
    attr_reader :status, :retry_after

    def initialize(status, message, retry_after = 0.0)
      super("#{status}: #{message}")
      @status = status
      @retry_after = retry_after
    end
  end

  # One keep-alive connection per client. Net::HTTP is not thread-safe:
  # give every thread its own client.
  class Client
    def initialize(key = ENV.fetch("SELLERMAGNET_API_KEY"))
      @key = key
      @http = Net::HTTP.new(API.host, API.port)
      @http.use_ssl = API.scheme == "https"
      @http.open_timeout = 10
      @http.read_timeout = 120 # the API allows a scrape up to 120 s
      @http.max_retries = 0    # Net::HTTP would silently re-send a timed-out GET; every send is billed
    end

    def get(endpoint, params)
      uri = URI.join(API, endpoint)
      uri.query = URI.encode_www_form(params)
      request = Net::HTTP::Get.new(uri)
      request["X-Api-Key"] = @key
      @http.start unless @http.started?
      response = @http.request(request)
      body = JSON.parse(response.body.to_s) rescue {} # an HTML error page is not JSON
      body = {} unless body.is_a?(Hash)
      return body["data"] if response.code == "200" && body["success"]

      raise ApiError.new(response.code.to_i, body.fetch("message", "request failed"), response["Retry-After"].to_f)
    end

    def lookup(asin, marketplace_id)
      p = get("amazon-product-lookup", asin: asin, marketplaceId: marketplace_id)["productInfo"]
      price = p.dig("buyBoxInfo", "price")
      {
        asin: p["asin"],
        title: p["title"],
        price: price.is_a?(Numeric) ? price : nil, # "N/A" = no featured offer
        currency: p.dig("buyBoxInfo", "currencyCode"),
        url: p["link"]
      }
    end
  end
end

if $PROGRAM_NAME == __FILE__
  p SellerMagnet::Client.new.lookup("B0CL61F39H", "ATVPDKIKX0DER")
end

Trimmed lookup response (example values)

{
  "success": true,
  "data": {
    "productInfo": {
      "asin": "B0CL61F39H",
      "title": "PlayStation®5 console (slim)",
      "buyBoxInfo": {"price": 444.99, "currencyCode": "USD", "sellerId": "Amazon", "amazonSellerId": "ATVPDKIKX0DER"},
      "link": "https://www.amazon.com/dp/B0CL61F39H"
    }
  }
}

buyBoxInfo.price is a decimal number, or the string "N/A" when no offer is featured - the client turns that into nil instead of zero. Every field is listed on the product lookup page.

How do I search Amazon by keyword in Ruby?

/api/amazon-search takes q, a marketplace ID and an optional count of up to 50. Results come from the first results page, sponsored ones included and flagged, and products without a displayed price are skipped. The price is a string, so parse it with BigDecimal rather than Float.

search.rb - organic results with exact prices

require "bigdecimal"
require_relative "seller_magnet"

client = SellerMagnet::Client.new
count = 20
raise ArgumentError, "count above 50 is a billed 400" unless (1..50).cover?(count)

data = client.get("amazon-search", q: "usb c charger", marketplaceId: "A1PA6795UKMFR9", count: count)
organic = data["searchResults"].reject { |r| r["sponsored"] }.map do |r|
  total = r.dig("listingPrice", "price", "total") # a string such as "19.99"
  { position: r["position"], asin: r["asin"], title: r["productTitle"], price: total && BigDecimal(total) }
end

organic.first(5).each do |row|
  puts [row[:position], row[:asin], row[:price] && row[:price].to_s("F"), row[:title].to_s[0, 60]].join("  ")
end

Which errors should a Ruby client retry?

Retry only what can succeed on a second try. A 429 from the per-key concurrency cap means it stayed full for 20 seconds; it is free and carries Retry-After: 2. A 500 is our internal error and its credit is refunded, so one retry costs nothing. 502 and 503 are billed per attempt, so wait at least the Retry-After and cap the attempts. A missing parameter is a free 400, but an endpoint's own 400 - such as a count above 50 - is billed. Add the retry helper to the bottom of seller_magnet.rb.

Status codes, billing and the right client reaction (September 2026).
StatusMeaningChargedClient action
200SuccessYesUse data
400Bad or missing parameterOnly an endpoint's own 400Fix the call
401 / 403Key or creditsNoStop
404Not on this marketplaceYesStore as missing
429Concurrency capNoWait retry-after
500Internal errorNo - refundedRetry once
502 / 503Upstream / breakerYesWait, then retry

seller_magnet.rb (continued) - retries only where a second attempt can help

module SellerMagnet
  RETRYABLE = [429, 500, 502, 503].freeze
  NETWORK_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED,
                    Errno::EPIPE, OpenSSL::SSL::SSLError, SocketError].freeze

  # Retries only what a second attempt can fix: 429 is free, a 500 is refunded
  # (retried once), 502 and 503 are billed per attempt.
  def self.with_retry(attempts: 4)
    internal_errors = 0
    (1..attempts).each do |attempt|
      begin
        return yield
      rescue ApiError => e
        internal_errors += 1 if e.status == 500
        raise if !RETRYABLE.include?(e.status) || attempt == attempts || internal_errors > 1

        sleep([2**attempt + rand, e.retry_after].max)
      rescue *NETWORK_ERRORS
        raise if attempt == attempts

        sleep(2**attempt)
      end
    end
  end
end
Blueprint table of API status codes with whether each is charged and how a Ruby client should react
Gold rows are the ones worth retrying.

How do I run many lookups in parallel?

Use a Queue and a handful of threads. Ruby releases the global VM lock while a thread waits on the network, so threads give real concurrency for API calls. Each thread builds its own client, because one Net::HTTP connection must not be shared. Five workers stay well under the per-key concurrency cap; there is no batch endpoint, and the bulk ASIN lookup guide covers pacing for thousands of products.

bulk.rb - five workers, one client each, results to CSV

require "csv"
require_relative "seller_magnet"

products = [%w[B0CL61F39H ATVPDKIKX0DER], %w[B0CLTBHXWQ APJ6JRA9NG5V4]] # [asin, marketplaceId]
jobs = Queue.new
products.each { |pair| jobs << pair }
rows = Queue.new

workers = Array.new(5) do
  Thread.new do
    client = SellerMagnet::Client.new # one connection per thread
    loop do
      asin, marketplace_id = begin
        jobs.pop(true)
      rescue ThreadError # queue empty
        break
      end
      rows << begin
        SellerMagnet.with_retry { client.lookup(asin, marketplace_id) }.merge(error: nil)
      rescue SellerMagnet::ApiError => e
        { asin: asin, error: e.status }
      rescue StandardError => e # network gave up after retries: keep the batch
        { asin: asin, error: e.class.name }
      end
    end
  end
end
workers.each(&:join)

CSV.open("products.csv", "w") do |csv|
  csv << %w[asin title price currency error]
  csv << rows.pop.values_at(:asin, :title, :price, :currency, :error) until rows.empty?
end
Blueprint table of the Ruby standard library pieces the client uses and what each one does
Five standard-library pieces, no gems to install on Ruby 2.6 to 3.3.

How do I use the client in Rails?

Save the client as lib/seller_magnet.rb: Zeitwerk maps that file name to SellerMagnet, and config.autoload_lib, present in apps generated by Rails 7.1 and later, loads it; older apps require "seller_magnet" in an initializer. Keep the key in Rails credentials and pass it in with SellerMagnet::Client.new(Rails.application.credentials.dig(:sellermagnet, :api_key)). Call it from a background job (Solid Queue, Sidekiq or GoodJob via Active Job), not from a controller: a lookup can take up to 120 seconds. Rescue ApiError for 400, 401, 403 and 404 inside the job, or use discard_on, and store the status - otherwise the job runner's own retries (Sidekiq defaults to 25) pay for the same 404 again. The error-handling guide covers the per-IP limit and the circuit breaker.

Frequently Asked Questions

Is there a Ruby gem for the SellerMagnet API?

No, and none is needed. Every endpoint is a plain GET with query parameters, so Net::HTTP and JSON from the standard library cover it.

Which Ruby version do I need?

Ruby 2.6 or later. On Ruby 3.4 and later, csv and bigdecimal are bundled gems, so list them in your Gemfile when you use Bundler.

What timeout should the client use?

A read_timeout of 120 seconds. The server allows a scrape that long, and a request the client abandons early is still charged.

Can threads share one client?

No. Net::HTTP connections are not thread-safe, so create one client per thread, as bulk.rb does.

Should I call the API inside a Rails controller?

No. Run it in a background job and store the result; a single lookup can take up to 120 seconds.

Bottom line: one Net::HTTP client with a 120-second read timeout, one exception type, retries for 429, 500, 502 and 503 only, and a thread per worker. The same pattern exists for Python and Node.js, and every endpoint is in the API documentation.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime