Home / Blog / Amazon Product API in PowerShell: Invoke-RestMethod, Retries and Billing

Amazon Product API in PowerShell: Invoke-RestMethod, Retries and Billing

Call an Amazon product API from PowerShell with Invoke-RestMethod: a small module for product lookup, offers and statistics, why PowerShell throws instead of returning a status code on a non-2xx response, and a retry policy that never pays twice for the same billed answer.

November 09, 2026
7 min read
SellerMagnet Team
Share & Bookmark
Terminal showing a PowerShell function catching HttpResponseException to read the status code of a failed SellerMagnet API call

To call an Amazon product API from PowerShell, send a GET request with Invoke-RestMethod to /api/amazon-product-lookup with an ASIN, a marketplace ID and your API key in the X-Api-Key header; the cmdlet parses the JSON body for you and returns it as an object. This quickstart builds a small module for three endpoints (lookup, offers and statistics), shows where Invoke-RestMethod hides the HTTP status code on a failed call, and adds a retry policy that only resends the requests a second attempt can fix.

Key Takeaways

  • Invoke-RestMethod throws on any non-2xx response instead of returning a status code - the status lives on $_.Exception.Response.StatusCode inside a catch block, as a HttpStatusCode enum value, not a plain integer.
  • Set -TimeoutSec 120: the API can take that long to answer, and an abandoned request is still billed.
  • Send the key in an -Headers hashtable (X-Api-Key), never in the URL as a query parameter.
  • Lookup sends the price as a number or the literal string "N/A"; statistics sends it as integer cents. Compare the lookup value to the string "N/A" before doing arithmetic with it.
  • Retry 429, a Retry-After-less 500, 502 and 503 only. A 404 is a billed answer; retrying it pays for the same answer again.

What do I need before I start?

  • PowerShell 7 or later (pwsh); Invoke-RestMethod ships with it, no module install needed. Windows PowerShell 5.1 also has the cmdlet, but its exception shape differs from the one used here (PowerShell 7's HttpResponseException).
  • An API key - a free account includes 150 credits, one per request.
  • The key in an environment variable, never hard-coded in a script you might commit.

Save the module, set the key (Linux/macOS; Windows uses setx)

export SELLERMAGNET_API_KEY="your-key-here"
pwsh -File main.ps1

How do I call the Amazon product API from PowerShell?

Keep the base URL, the key and the timeout together and build one function per endpoint on top of a single request helper. The key goes into the -Headers hashtable, never the URL: a key left in a URL ends up in shell history and request logs, and the header vs query string guide shows where that leaks. Test-SMAsin filters a malformed ASIN before it can cost a credit on lookup or offers, or trigger a billed 400 on statistics.

SellerMagnet.psm1 - config, the ASIN check and one request helper

# SellerMagnet API client for PowerShell: Invoke-RestMethod + a billing-aware
# retry wrapper. One dependency: PowerShell 7+ itself (Invoke-RestMethod ships
# with it, no module install needed).

function Get-SMConfig {
    <#
    .SYNOPSIS
    Reads SELLERMAGNET_API_KEY (required) and SELLERMAGNET_API_BASE (optional,
    defaults to the public API, overridden here to point at a fake server in
    tests). Fails fast with a clear message instead of sending an empty key.
    #>
    $key = $env:SELLERMAGNET_API_KEY
    if ([string]::IsNullOrWhiteSpace($key)) {
        throw "Set SELLERMAGNET_API_KEY before calling the API."
    }
    $base = $env:SELLERMAGNET_API_BASE
    if ([string]::IsNullOrWhiteSpace($base)) { $base = "https://sellermagnet-api.com/api" }
    return [PSCustomObject]@{ Key = $key; Base = $base }
}

function Test-SMAsin {
    <#
    .SYNOPSIS
    True for a plausible Amazon ASIN: 10 upper-case alphanumeric characters.
    .DESCRIPTION
    Uses \A...\z, not ^...$: a bare ^...$ in PowerShell/.NET regex still
    matches a string with one trailing newline (verified directly with
    'B0CL61F39H' + "`n" on this host, the same trap documented for the Perl
    and Elixir quickstarts), because $ matches end-of-string OR right before
    a single trailing \n even without the multiline option. \A and \z anchor
    to the true start/end with no such exception.
    #>
    param([Parameter(Mandatory)][string]$Asin)
    return $Asin -cmatch '\A[A-Z0-9]{10}\z'
}

Why does a successful call look different from a failed one?

Because Invoke-RestMethod does not return a status code at all on a non-2xx response: it throws a Microsoft.PowerShell.Commands.HttpResponseException instead, unlike requests in Python or a status field on a returned struct in Go. The real HTTP status lives on $_.Exception.Response.StatusCode inside the catch block, as a System.Net.HttpStatusCode enum value (NotFound, not 404) - cast it with [int] to get the number back. This was verified directly against a real HTTP 404 on this host, not assumed from another language's quickstart: PowerShell's own error shape is genuinely different from every prior post in this series. A connection failure (DNS, refused, timeout) throws a different exception with no .Response at all, so the catch block checks for that case separately instead of assuming every exception carries a status code.

Invoke-SMRequest - reading the status out of the caught exception

function Invoke-SMRequest {
    <#
    .SYNOPSIS
    One GET against the SellerMagnet API, no retry: callers that need retries
    use Invoke-SMRequestWithRetry below. Returns the decoded `data` object on
    success; on failure returns $null and sets $Result (ref) with Status,
    Message and RetryAfterSeconds, the same split every other quickstart in
    this series uses so a caller never has to parse an exception to find the
    status code.
    .DESCRIPTION
    Invoke-RestMethod throws on any non-2xx status (HttpResponseException in
    PowerShell 7+), unlike requests.Session.get() in Python or a return-coded
    response struct in C/Go, so the status must be read out of the caught
    exception, not off a return value - verified directly on this host, not
    assumed from the other quickstarts' explicit-return-value shape.
    #>
    param(
        [Parameter(Mandatory)][string]$Endpoint,
        [Parameter(Mandatory)][hashtable]$Params,
        [Parameter(Mandatory)][ref]$Result
    )
    $cfg = Get-SMConfig
    $query = ($Params.GetEnumerator() | ForEach-Object {
        "$([uri]::EscapeDataString($_.Key))=$([uri]::EscapeDataString([string]$_.Value))"
    }) -join "&"
    $uri = "$($cfg.Base)$Endpoint`?$query"

    $Result.Value = [PSCustomObject]@{ Status = -1; Message = ""; RetryAfterSeconds = $null; IsTransport = $false }
    try {
        $resp = Invoke-RestMethod -Uri $uri -Headers @{ "X-Api-Key" = $cfg.Key } -TimeoutSec 120 -Method Get
        $Result.Value.Status = 200
        if (-not $resp.success) {
            # defensive: a 200 with success:false is not documented, but never silently treated as data
            $Result.Value.Message = "unexpected success:false on HTTP 200"
            return $null
        }
        return $resp.data
    } catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        $status = [int]$_.Exception.Response.StatusCode
        $Result.Value.Status = $status
        $retryAfterHeader = $_.Exception.Response.Headers.RetryAfter
        if ($retryAfterHeader -and $retryAfterHeader.Delta) {
            $Result.Value.RetryAfterSeconds = [int]$retryAfterHeader.Delta.Value.TotalSeconds
        }
        $msg = $null
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            try { $msg = ($_.ErrorDetails.Message | ConvertFrom-Json).message } catch { $msg = $_.ErrorDetails.Message }
        }
        $Result.Value.Message = if ($msg) { $msg } else { $_.Exception.Message }
        return $null
    } catch {
        # DNS failure, connection refused, timeout: no HTTP answer at all, never billed, never retried
        # the same way as a billed 5xx (the request may not have reached the server).
        $Result.Value.IsTransport = $true
        $Result.Value.Message = $_.Exception.Message
        return $null
    }

Trimmed lookup response (example values from the API docs)

{
  "success": true,
  "data": {
    "productInfo": {
      "asin": "B0CL61F39H",
      "buyBoxInfo": {"price": 444.99, "currencyCode": "USD", "currencySymbol": "$"}
    }
  }
}

productInfo.buyBoxInfo.price from product lookup is a number or the literal string "N/A" when no offer is featured. Invoke-RestMethod already parsed the JSON, so the PowerShell value really is a [string] in that case and a [double] otherwise - comparing it to "N/A" before any arithmetic keeps a missing price from silently becoming 0 through a failed numeric coercion.

Why is the statistics price a different number?

Because /amazon-product-statistics reports buyBoxPrice in integer minor units: 41800 means 418.00. Lookup and offers report decimal units, and the lookup price excludes shipping while the statistics and offers prices are landed (item plus shipping), so 409.99 from lookup and 41800 from statistics can describe the same offer. Divide by 100 for the major unit, except on amazon.co.jp, where the values are whole yen. More detail is in the cents vs decimal guide. Statistics also answers a free 400 (not billed, not retried) outside the 11 marketplaces with full price/BSR history.

JSON fields and how the PowerShell client reads each one safely (November 2026).
FieldEndpointJSON valuePowerShell handling
productInfo.buyBoxInfo.pricelookup444.99 or "N/A"read as-is; compare to "N/A" before using as a number
offers[].totalPriceoffers418.00 (decimal)landed price: item + shipping
buyBoxoffers{} when nobody holds itcheck for a totalPrice property before reading one
buyBoxPricestatistics41800 (integer cents) or nulldivide by 100 for the major unit
offers[].inventoryoffers30 (cart cap)add-to-cart maximum, not the seller's stock
Table mapping SellerMagnet API price fields to how the PowerShell client reads each one safely, including the statistics price in integer cents
Decimal on lookup and offers, integer cents on statistics: compare before you compute.

What should a PowerShell client retry?

Retry only what a second attempt can fix. A 429 from the per-key concurrency cap is free and carries Retry-After; $_.Exception.Response.Headers.RetryAfter.Delta gives that wait as a TimeSpan. A 500 without Retry-After is refunded, so one retry costs nothing beyond the retry itself; a 500 that carries Retry-After stays charged. 502 and 503 are billed on every attempt, so wait at least the Retry-After and cap the attempts. Invoke-RestMethod has no retry logic of its own - a thrown exception is never resent automatically, so this loop decides what gets sent twice and nothing else does.

Status codes, billing and the right PowerShell client reaction (November 2026).
StatusMeaningChargedClient action
200SuccessYesRead data
400 gateMissing parameterNoFix the call
400 endpointUncovered marketplace on statisticsYesFix the call
401 / 403Key or creditsNoStop the run
404Not on this marketplaceYesRecord, never retry
429Concurrency capNoWait Retry-After
500Internal error, no Retry-AfterNo, refundedRetry once
502 / 503Upstream error / breakerYesWait, retry, cap

Invoke-SMRequestWithRetry - retries that respect billing

function Invoke-SMRequestWithRetry {
    <#
    .SYNOPSIS
    Retries only what a second attempt can fix: 429 (free, honours
    Retry-After), a 500 without Retry-After (refunded, retried once), and
    502/503 (billed on every attempt, capped at 4 total). Never retries 400,
    401, 403 or 404 - those are either free-and-unfixable or billed-and-final.
    #>
    param(
        [Parameter(Mandatory)][string]$Endpoint,
        [Parameter(Mandatory)][hashtable]$Params,
        [int]$MaxAttempts = 4
    )
    $attempt = 0
    while ($true) {
        $attempt++
        $result = $null
        $data = Invoke-SMRequest -Endpoint $Endpoint -Params $Params -Result ([ref]$result)
        if ($null -ne $data) { return [PSCustomObject]@{ Data = $data; Result = $result; Attempts = $attempt } }
        if ($result.IsTransport) { return [PSCustomObject]@{ Data = $null; Result = $result; Attempts = $attempt } }

        $retryable = ($result.Status -eq 429) -or
                     ($result.Status -eq 500 -and -not $result.RetryAfterSeconds) -or
                     ($result.Status -eq 502) -or ($result.Status -eq 503)
        if (-not $retryable -or $attempt -ge $MaxAttempts) {
            return [PSCustomObject]@{ Data = $null; Result = $result; Attempts = $attempt }
        }
        $wait = if ($result.RetryAfterSeconds) { $result.RetryAfterSeconds } else { $attempt }
        Start-Sleep -Seconds $wait
    }
}
Sequence of a PowerShell client retrying a free 429 after Retry-After, then recording a billed 404 without retrying
Wait and retry a free 429; record a billed 404 and move on.

Run lookup and statistics for five products

main.ps1 checks each ASIN's format first with Test-SMAsin, because a malformed ASIN still costs a credit on lookup and a billed 400 on statistics. It stops a row after a failed lookup instead of calling statistics for an ASIN that was never confirmed to exist on that marketplace, and prints one row per product.

main.ps1 - lookup then statistics, one row per product

# Demo run: lookup, offers and statistics for five products, same shape as
# the C/Perl/Elixir quickstarts' main program (illustrative data from the
# fake server, captured verbatim into the post's example-output block).
Import-Module "$PSScriptRoot/SellerMagnet.psm1" -Force

$products = @(
    @{ Asin = "B0CL61F39H"; MarketplaceId = "ATVPDKIKX0DER" },
    @{ Asin = "B0CLTBHXWQ"; MarketplaceId = "APJ6JRA9NG5V4" },
    @{ Asin = "B0CL61F39H"; MarketplaceId = "A2VIGQ35RCS4UG" },
    @{ Asin = "B0NOTFOUND"; MarketplaceId = "ATVPDKIKX0DER" },
    @{ Asin = "B0CL61F39"; MarketplaceId = "ATVPDKIKX0DER" }
)

$billed = 0
Write-Host ("{0,-11} {1,-16} {2,-11} {3,-11} {4}" -f "asin", "marketplace", "lookup", "stats(cents)", "note")
foreach ($p in $products) {
    if (-not (Test-SMAsin $p.Asin)) {
        Write-Host ("{0,-11} {1,-16} not an ASIN, skipped (0 credits)" -f $p.Asin, $p.MarketplaceId)
        continue
    }
    $lookup = Get-SMProduct -Asin $p.Asin -MarketplaceId $p.MarketplaceId
    $billed++
    if ($null -eq $lookup.Data) {
        Write-Host ("{0,-11} {1,-16} {2}: {3}" -f $p.Asin, $p.MarketplaceId, $lookup.Result.Status, $lookup.Result.Message)
        continue
    }
    $price = $lookup.Data.productInfo.buyBoxInfo.price
    $stats = Get-SMStatistics -Asin $p.Asin -MarketplaceId $p.MarketplaceId
    $billed++
    $statsText = if ($null -eq $stats.Data) { "not covered (free 400)" } else { "$($stats.Data.buyBoxPrice)" }
    Write-Host ("{0,-11} {1,-16} {2,-11} {3,-11} {4}" -f $p.Asin, $p.MarketplaceId, $price, $statsText, "")
}
Write-Host "credits charged: $billed"

Program output (illustrative data, from the local fake server this code was tested against)

asin        marketplace      lookup      stats(cents) note
B0CL61F39H  ATVPDKIKX0DER    444.99      44499       
B0CLTBHXWQ  APJ6JRA9NG5V4    N/A         41800       
B0CL61F39H  A2VIGQ35RCS4UG   444.99      not covered (free 400) 
B0NOTFOUND  ATVPDKIKX0DER    404: Product not found
B0CL61F39   ATVPDKIKX0DER    not an ASIN, skipped (0 credits)
credits charged: 7

The credit math for that run: 7 requests, 7 credits (none of the five scenarios hit a free 429/500/gate-400 in this particular run; the retry policy that avoids paying for those is exercised separately in the test suite below). The fourth row's 404 ended its row after one credit, without calling statistics, and the fifth row's 9-character ASIN never left the script. The second row shows the point of the table above: lookup reports "N/A" (no featured offer at that instant) while statistics still has a recorded price of 41800 cents (418.00) from its own history. To test changes without spending credits, point SELLERMAGNET_API_BASE at a local fake HTTP server; that is how this code was tested, with no network call ever reaching the real API.

How was this tested without a .NET test framework?

Against a real local HTTP/1.1 server built on System.Net.HttpListener (fake_server.ps1), the same "real local server, no hand-written mock of the client" convention as the C, Swift, Rust, Perl and Elixir quickstarts in this series - never a hand-written stand-in for SellerMagnet.psm1 itself. scenarios.ps1 runs 36 plain assertions against the unmodified module: the happy path, "N/A" safety, a billed 404 that is never retried, a 500 that recovers on retry, a 429 that waits for its Retry-After before retrying, a 502 capped at 4 attempts, 401/403 stopping at once, a free gate 400, empty offers/buyBox {}, the cents-vs-decimal equivalence, the ASIN trailing-newline trap (proven directly, not assumed from the Perl/Elixir posts), a transport-level connection failure, and a missing API key failing closed. All 36 pass.

pwsh -File scenarios.ps1 (36/36 assertions pass)

36 passed, 0 failed

Frequently Asked Questions

Is there an official PowerShell SDK for the SellerMagnet API?

No, and none is needed. Every endpoint is a plain GET with query parameters and a JSON body that Invoke-RestMethod already parses, so a client fits in well under 150 lines.

Why does my script's catch block never see a status code?

Because Invoke-RestMethod throws on a non-2xx response instead of returning one. Catch Microsoft.PowerShell.Commands.HttpResponseException and read [int]$_.Exception.Response.StatusCode, not a return value.

Does Invoke-RestMethod retry anything on its own?

No. A failed call throws once and stops; nothing in PowerShell resends it. The retry loop in this post is the only thing deciding what gets sent twice.

Why compare the lookup price to the string "N/A" instead of just using it as a number?

Because productInfo.buyBoxInfo.price really is the string "N/A" when no offer is featured. PowerShell's loose typing will not raise an error if you skip the check, it will just produce a wrong number from the comparison or arithmetic later.

What PowerShell version does this need?

PowerShell 7 or later. The exception type and the way the status code is read (this post's main point) are specific to PowerShell 7's Invoke-RestMethod; Windows PowerShell 5.1 throws a different exception shape.

Bottom line: one Invoke-RestMethod call per request with a 120-second timeout and the key in an X-Api-Key header, a caught HttpResponseException to read the status code PowerShell hides on a failed call, explicit "N/A" checks before any price arithmetic, and retries for 429, 500, 502 and 503 only. The same client shape exists for Python and Go, 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