Home / Blog / Amazon Product API in Zapier: Keep the Key Out of the Zap

Amazon Product API in Zapier: Keep the Key Out of the Zap

Call the SellerMagnet API from a Zap without leaving the X-Api-Key header sitting in a Zap step's plaintext fields: set it once in Storage by Zapier instead, then fetch it at run time from a Code by Zapier Python step that also adds the series' billing-aware retry and a StoreClient dedup against Zapier's own "Replay entire Zap run" double-billing trap.

November 11, 2026
8 min read
SellerMagnet Team
Share & Bookmark
Terminal window showing a Zapier Code by Zapier Python step calling the SellerMagnet API

To call the SellerMagnet API from a Zap without leaving the X-Api-Key header sitting in a Zap step's own plaintext fields, set the key once in Storage by Zapier instead of typing it into a Webhooks by Zapier action. A Code by Zapier Python step then fetches the key at run time with StoreClient, applies the billing-aware retry policy the rest of this series uses, and dedupes against a real Zapier-specific trap: "Replay entire Zap run" re-bills a call that already succeeded.

Key Takeaways

  • Storage by Zapier's Set Value/Get Value actions hold the API key outside any single Zap step; a Code step fetches it at run time with StoreClient instead of it sitting in a Webhooks by Zapier action's plaintext fields.
  • Code by Zapier's Python steps have no Zapier SDK access and cannot read another action's stored connection credential, so an API by Zapier connection cannot feed a Code step either - Storage by Zapier is the mechanism built for this.
  • A normal replay (manual or Autoreplay) only re-attempts steps that stopped on an errored status; "Replay entire Zap run" re-executes every step, successful ones included, and bills them again.
  • A StoreClient dedup keyed on a stable id already present in the trigger data (not a value generated inside the Zap) survives a full-run replay and blocks the re-bill.
  • Billing mirrors the rest of the series: free 429 (Retry-After)/401/403/gate-400, a 500 is refunded (the client still retries it once), 502/503 billed-capped-at-4, 404 billed-not-retried.

Where should the API key actually live in a Zap?

Zapier's own "Ways to make API requests in Zapier" (help.zapier.com, checked live) shows Webhooks by Zapier's Custom Request action supporting only Basic auth or none; a manually added X-Api-Key header there sits in that Zap step's own fields in plaintext, visible to anyone who opens or exports the Zap. API by Zapier avoids that by keeping an API key in its own connection - but that connection can only feed an API by Zapier action step, never a Code step, because Code by Zapier's Python steps have no Zapier SDK access (the SDK is JavaScript-only, per "Code by Zapier", help.zapier.com) and no way to read another connection's stored credential. Since the retry policy below needs a Code step, the right place for the key is Storage by Zapier: "Save and retrieve data from Zap workflows using Storage by Zapier" (help.zapier.com) documents a "Set Value" action that stores a value under a key, and a "Get Secret" action that hands a Code step the store secret it needs to read that value back with StoreClient - the same utility this guide already uses for the replay dedup.

Where the credential actually lives, per help.zapier.com.
SetupHow it is enteredWhere it lives afterwards
Webhooks by Zapier (Custom Request)Type the header into the step's own fieldsStays in that Zap step forever, in plaintext, visible to anyone who opens or exports the Zap
Storage by Zapier + a Code stepSet the value once with a temporary Set Value action, then delete that actionLives only in your Storage by Zapier account; later steps fetch it at runtime with StoreClient.get(), it is never shown in any Zap step again
  1. Create a Storage by Zapier accountAdd any Storage by Zapier action, connect a new account, and enter a UUID4 as the store secret (this is the value the Code step below will need, not the SellerMagnet key itself).
  2. Set the key onceAdd a temporary Storage by Zapier step, event Set Value, Key sellermagnet_api_key, Value your real key. Test it once.
  3. Delete that stepRemove the Set Value action from the Zap. The value stays in your Storage by Zapier account; nothing in the Zap's own steps shows it again.
  4. Read it back at run timeIn the Code by Zapier step below, StoreClient(store_secret).get("sellermagnet_api_key") fetches it fresh on every run.

Why does the retry logic need its own Code step?

A plain API Request action returns whatever SellerMagnet sends and stops there: no branching on the status code, no backoff, no memory of a previous run. The series' billing-aware retry - free 401/403/429 (honour Retry-After), a refunded 500 the client still retries once, 502/503 billed on every attempt and capped at 4, a billed 404 not retried at all - has to live somewhere that can branch and loop, which inside a Zap means a Code by Zapier Python step. "Using Code by Zapier" and "Use Python code in Zap workflows" (help.zapier.com, checked live) confirm this Python environment already provides requests and a StoreClient get/set utility with no ability to import your own local files, so the whole policy has to be one script, the same constraint the AWS Lambda and Cloudflare Worker posts in this series hit for their own platform reasons.

sellermagnet_zapier.py - paste into a Code by Zapier Python step (Run Python)

"""sellermagnet_zapier.py - paste into a Code by Zapier Python step (event: Run Python).
Code by Zapier pre-injects `requests` and `StoreClient`; there is no way to import a local
module, so the whole billing-aware retry and the replay-safe dedup live in this one script.
Input Data: asin, request_id (a stable id from the trigger, e.g. a sheet row id - never a
value generated inside the Zap), store_secret (the UUID4 from your Storage by Zapier account,
used both for the dedup store and to fetch the API key set there with a one-time Set Value
action - see "Store the API key once, outside the Zap's own steps" above).
"""
import re
import time

API = "https://sellermagnet-api.com/api/amazon-product-lookup"
MARKETPLACE_ID = "ATVPDKIKX0DER"                  # amazon.com
RETRYABLE = {429, 500, 502, 503}
ASIN_RE = re.compile(r"^[A-Z0-9]{10}$")


def classify_billed(status: int) -> bool:
    """True if a response at this status is charged, from the status-codes/billing table.
    A gate 400, 401/403/429 and 500 are free; 200/404/502/503 are billed."""
    if status in (400, 401, 403, 429, 500):
        return False
    return True


def call_with_retry(asin: str, api_key: str, attempts: int = 4) -> dict:
    internal_errors = 0
    credits_spent = 0
    for attempt in range(1, attempts + 1):
        resp = requests.get(API, params={"asin": asin, "marketplaceId": MARKETPLACE_ID},
                             headers={"X-Api-Key": api_key}, timeout=120)
        try:
            body = resp.json()
        except ValueError:
            body = {}
        if classify_billed(resp.status_code):
            credits_spent += 1
        if resp.status_code == 200 and body.get("success"):
            return {"status": "ok", "data": body["data"], "attempts": attempt, "credits_spent": credits_spent}
        if resp.status_code == 500:
            internal_errors += 1
        if resp.status_code not in RETRYABLE or attempt == attempts or internal_errors > 1:
            return {"status": "error", "http_status": resp.status_code,
                     "message": body.get("message", "request failed"), "attempts": attempt,
                     "credits_spent": credits_spent}
        try:
            wait = float(resp.headers.get("Retry-After", 0) or 0)
        except ValueError:
            wait = 0.0
        time.sleep(max(min(2 ** attempt, 30), wait))
    return {"status": "error", "http_status": None, "message": "exhausted", "attempts": attempts,
             "credits_spent": credits_spent}


def run(input_data):
    store = StoreClient(input_data["store_secret"])
    asin = (input_data.get("asin") or "").strip().upper()
    api_key = store.get("sellermagnet_api_key")           # set once via a Storage by Zapier action
    request_id = input_data.get("request_id", "")         # stable upstream id - NOT generated inside the Zap
    if not ASIN_RE.match(asin):
        return {"skipped": True, "reason": "malformed_asin", "credits_spent": 0}
    dedup_key = f"billed:{request_id}:{asin}"
    if request_id and store.get(dedup_key):
        return {"skipped": True, "reason": "already_billed_this_request_id", "credits_spent": 0}
    result = call_with_retry(asin, api_key)
    if request_id and result["credits_spent"] > 0:
        store.set(dedup_key, True)
    return result


if __name__ == "__main__":
    # Local test only: StoreClient/requests are injected globals inside the real Code by Zapier
    # editor; this is not how you paste it there (see the one-liner callout below for that).
    print(run({"asin": "B0CL61F39H", "request_id": "row1",
               "store_secret": "a-uuid4-you-generate-once"}))

Zapier's own editor expects the LAST line of a Run Python step to return its output, not a function definition at the top level; the file above is written and tested as an importable module so every function can be unit-tested on its own, but what actually goes in the Zapier code box is everything above unchanged, with its if __name__ block dropped and this one line in its place.

Last line to paste in the Zapier editor instead of the if __name__ block above

return run(input_data)

Illustrative run output (same script, four calls: fresh, a replay of the first, a different request_id, a malformed ASIN)

--- call 1: fresh request_id="row1" ---
{'status': 'ok', 'attempts': 1, 'credits_spent': 1, 'data': {...}}
--- call 2: "Replay entire Zap run" reuses request_id="row1" ---
{'skipped': True, 'reason': 'already_billed_this_request_id', 'credits_spent': 0}
--- call 3: same ASIN, different request_id="row2" ---
{'status': 'ok', 'attempts': 1, 'credits_spent': 1, 'data': {...}}
--- call 4: malformed asin="short" ---
{'skipped': True, 'reason': 'malformed_asin', 'credits_spent': 0}
billed SellerMagnet calls: 2 (the replay and the malformed ASIN cost nothing)
Blueprint comparing a normal Zap replay against Replay entire Zap run and how each interacts with SellerMagnet billing
Only the full-run replay re-bills a call that already succeeded; the dedup targets exactly that case.

How does the StoreClient dedup actually block a re-bill?

run() builds a dedup key from request_id and the ASIN together, where request_id has to already exist in the trigger data - a spreadsheet row's own id, a form submission id, anything the upstream app assigned before the Zap ever ran. After a billed call, the key is written to the store. On a normal new trigger event, request_id is new, so the store has nothing under that key and the call goes through. On a "Replay entire Zap run" of the SAME event, request_id is identical (the replay reuses the original trigger data), the store already holds it, and the step returns {'skipped': True, ...} without calling SellerMagnet at all - no second credit spent for the same event. A value generated inside the Zap itself (the run's own timestamp, a freshly made UUID) would NOT work here, because it differs between the original run and its replay and the dedup would never fire.

SellerMagnet status codes, billing and the Code step's reaction.
StatusMeaningChargedCode step action
200successYesUse data, store the request_id/asin key
400 (bad asin)rejected before any callNoFix the input
400 (gate)marketplace not coveredNoFix 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, capped at 4

The 400 (bad asin) row is a client check, not an API one

/amazon-product-lookup does not validate ASIN format server-side at all; a malformed ASIN sent through would be billed like any other call. The regex in run() is what keeps a typo from ever reaching SellerMagnet, not a free 400 the API itself returns.

Blueprint flow from a Zap trigger through a Storage-backed Code step to the SellerMagnet API, with a StoreClient dedup gate
The dedup gate sits before the API call, so a replayed event never reaches SellerMagnet a second time.

request_id has to come from the trigger, not the Zap

Map request_id to a field the trigger app already assigned - a spreadsheet row id, a form response id, a database primary key. A value you generate with a Formatter step inside the Zap (a new timestamp or UUID) is created fresh on every run, including a full-run replay, so it defeats the dedup entirely.

Frequently Asked Questions

Why not pass the API key straight into the Code step as Input Data?

That is functionally the same plaintext problem as Webhooks by Zapier: whatever you map into a Code step's Input Data is visible in that step's own configuration. Storage by Zapier's Set Value/Get Value actions (help.zapier.com) exist precisely to hold a value like this outside any single step, fetched only at run time.

Can't a Code step read a connection's credential directly, the way API by Zapier does?

No. Code by Zapier's Python steps have no access to the Zapier SDK (JavaScript-only per "Code by Zapier", help.zapier.com) and no way to read another action's stored connection credential. If you want API by Zapier's own connection-based auth, the HTTP call itself has to be an API by Zapier action step, not a Code step - and that path loses the branching this guide's retry policy needs.

Does replaying a Zap run from Zap History re-bill a successful SellerMagnet call?

Not a normal replay: Zapier only re-attempts steps that stopped on an errored status, so an already-successful API call step is skipped. "Replay entire Zap run", a separate option in the Zap editor, re-executes every step including successful ones and bills them again - the StoreClient dedup below exists specifically for that second case.

Why key the dedup on a request_id from the trigger, not something generated in the Zap?

"Replay entire Zap run" reuses the SAME trigger data to start a new run, so a value generated inside the Zap (a timestamp, a random id) would differ between the original run and the replay and the dedup would never catch it. A value that already existed in the trigger data - a sheet row's own id, a form submission id - stays identical across the replay, which is what makes it work as a dedup key.

What happens if I don't pass a request_id at all?

The Code step still works; it just never deduplicates, so every run (including a full Zap replay) bills again. That is fine for a one-off manual test, not for a scheduled or triggered Zap that might get replayed.

Does Code by Zapier need an import requests line?

No. requests and the StoreClient utility are already available inside a Code by Zapier Python step; adding the import line yourself is harmless but unnecessary.

Bottom line: set the API key once in Storage by Zapier instead of a Webhooks by Zapier step's plaintext fields, and put the retry and dedup logic in one Code by Zapier Python step that fetches the key with StoreClient and keys the dedup on a stable id from the trigger. The same billing-aware retry policy, in languages that run outside Zapier, is in the Python quickstart and the rate limits and retries guide; a free account includes 150 credits to try it.

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