To call the SellerMagnet API from AWS Lambda without packaging any dependencies, write the handler with urllib.request from the standard library instead of requests - the managed Python 3.12 runtime preinstalls only boto3/botocore, not requests or urllib3. Expose the function through a Lambda Function URL rather than API Gateway, because SellerMagnet's own scrape can take up to 120 seconds while API Gateway's integration timeout is fixed at 29 seconds and cannot be raised for edge-optimized APIs.
Key Takeaways
- AWS Lambda's Python runtime ships boto3/botocore only; requests/urllib3 need a package or layer, so this handler uses urllib.request and deploys as one file.
- API Gateway's REST/HTTP integration timeout is 50ms-29s and cannot be raised at all for edge-optimized APIs; a Lambda Function URL instead follows the function's own 1-900s timeout.
- Set the Lambda function's own timeout to at least 130 seconds: SellerMagnet's documented scrape allowance is 120 seconds, and a timeout that races it risks a 504 on a request that SellerMagnet already billed.
- Read the API key from an environment variable (SELLERMAGNET_API_KEY), set in the function's configuration, never hardcoded in the handler.
- Validate the ASIN format before calling out: a malformed ASIN is rejected for free, a real 404 (wrong marketplace) is billed and should not be retried.
Why doesn't a plain import requests work in Lambda?
AWS documents exactly which packages the managed Python runtimes preinstall, and requests is not one of them - only boto3 and botocore, for calling other AWS services. A handler that imports requests without bundling it in the deployment package or a layer fails at import time with Runtime.ImportModuleError. The standard library's urllib.request needs neither: the function below is a single .py file you can paste straight into the Lambda console's inline code editor.
lambda_function.py - one file, no deployment package
"""lambda_function.py - call the SellerMagnet API from an AWS Lambda function
using only the Python standard library (urllib.request), so the function needs no
deployment package or Lambda layer - the default python3.12 runtime ships no
third-party HTTP client (requests/urllib3 are NOT preinstalled; only boto3/botocore
are), so a single-file handler with no "import requests" deploys straight from the
Lambda console or `zip -r function.zip lambda_function.py`.
"""
from __future__ import annotations
import json
import os
import random
import re
import time
import urllib.error
import urllib.parse
import urllib.request
API = "https://sellermagnet-api.com/api"
RETRYABLE = {429, 500, 502, 503}
class ApiError(Exception):
def __init__(self, status: int, message: str, retry_after: float = 0.0):
super().__init__(f"{status}: {message}")
self.status, self.retry_after = status, retry_after
def _get(endpoint: str, timeout: float, **params) -> dict:
"""One GET, key in the X-Api-Key header, 120s default timeout (the server allows
a scrape that long; a client that gives up earlier has still paid for it)."""
url = f"{API}/{endpoint}?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers={"X-Api-Key": os.environ["SELLERMAGNET_API_KEY"]})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read())
if resp.status == 200 and body.get("success"):
return body["data"]
raise ApiError(resp.status, body.get("message", "request failed"))
except urllib.error.HTTPError as err:
try:
body = json.loads(err.read())
except ValueError:
body = {}
try:
wait = float(err.headers.get("Retry-After", 0))
except (TypeError, ValueError):
wait = 0.0
raise ApiError(err.code, body.get("message", "request failed"), wait) from None
def get_with_retry(endpoint: str, attempts: int = 4, timeout: float = 120.0, **params) -> dict:
"""Same billing-aware policy as every other quickstart: retry 429/500/502/503 only,
a second 500 in a row is reported (one refund, not two), 404/gate-400 never retried."""
internal_errors = 0
for attempt in range(1, attempts + 1):
try:
return _get(endpoint, timeout, **params)
except ApiError as err:
internal_errors += err.status == 500
if err.status not in RETRYABLE or attempt == attempts or internal_errors > 1:
raise
time.sleep(min(max(2 ** attempt, err.retry_after) + random.uniform(0, 1), 30))
except (urllib.error.URLError, TimeoutError):
if attempt == attempts:
raise
time.sleep(min(2 ** attempt, 30))
def lookup(asin: str, marketplace_id: str, timeout: float = 120.0) -> dict:
p = get_with_retry("amazon-product-lookup", asin=asin, marketplaceId=marketplace_id,
timeout=timeout)["productInfo"]
price = p["buyBoxInfo"].get("price")
return {
"asin": p["asin"],
"title": p["title"],
"price": price if isinstance(price, (int, float)) else None,
"currency": p["buyBoxInfo"].get("currencyCode"),
"url": p["link"],
}
ASIN_OK = re.compile(r"\A[A-Z0-9]{10}\Z")
def lambda_handler(event: dict, context) -> dict:
"""Entry point for a Lambda Function URL (NOT an API Gateway REST proxy - see the
29-second integration-timeout note in the post). `event` is the Function URL
payload: query string params under `queryStringParameters`."""
params = (event or {}).get("queryStringParameters") or {}
asin = (params.get("asin") or "").strip().upper()
marketplace_id = params.get("marketplaceId") or "ATVPDKIKX0DER"
if not ASIN_OK.match(asin):
return {"statusCode": 400, "body": json.dumps({"error": "asin must be 10 upper-case letters/digits"})}
# SELLERMAGNET_TIMEOUT lets the function's own Lambda timeout (configured separately,
# up to 900s) and the HTTP client timeout be tuned together without a code change.
timeout = float(os.environ.get("SELLERMAGNET_TIMEOUT", "120"))
try:
data = lookup(asin, marketplace_id, timeout=timeout)
except ApiError as err:
return {"statusCode": err.status, "body": json.dumps({"error": err.status, "message": str(err)})}
except (urllib.error.URLError, TimeoutError) as err:
return {"statusCode": 504, "body": json.dumps({"error": "upstream_timeout", "message": str(err)})}
return {"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps(data)}
Demo run (same handler, 3 requests, 1 rejected for free)
GET ?asin=B0CL61F39H
-> 200 {"asin": "B0CL61F39H", "title": "PlayStation\u00ae5 console (slim)", "price": 444.99, "currency": "USD", "url": "https://www.amazon.com/dp/B0CL61F39H"}
GET ?asin=short-id
-> 400 {"error": "asin must be 10 upper-case letters/digits"}
GET ?asin=B000000000
-> 404 {"error": 404, "message": "404: Product not found"}
billed SellerMagnet calls: 2 (the malformed ASIN never reached the API)

What does the handler actually send and check?
The handler reads asin and an optional marketplaceId from the Function URL's query string, rejects anything that is not 10 upper-case letters or digits before spending a credit, then calls /amazon-product-lookup with the key in the X-Api-Key header and a timeout read from SELLERMAGNET_TIMEOUT (120 seconds by default, matching the Python quickstart's convention). buyBoxInfo.price can be the string "N/A" when no offer is featured - the handler maps that to null, not 0, so a client never reads a missing offer as a free product.
| Status | Meaning | Charged | Handler action |
|---|---|---|---|
200 | success | Yes | Use data |
400 (bad asin) | rejected before any call | No | Fix the input |
400 (gate) | marketplace not covered | No | Fix the call |
401 / 403 | key or credits | No | Stop |
404 | not on this marketplace | Yes | Store as missing |
429 | concurrency cap | No | Wait retry-after |
500 | internal error | No - refunded | Retry once |
502 / 503 | upstream / breaker | Yes | Wait, then retry, capped at 4 |

How do I deploy and configure it?
- Lambda console -> Create function -> Author from scratch -> Runtime: Python 3.12.
- Paste
lambda_function.pyinto the inline editor (norequirements.txt, no layer). - Configuration -> Environment variables -> add
SELLERMAGNET_API_KEYwith your key. - Configuration -> General configuration -> Timeout -> set to at least 2 min 10 sec (130s).
- Configuration -> Function URL -> Create function URL -> auth as required by your use case.
- A cold start adds to the first request's latency (interpreter + handler init); it does not change SellerMagnet's own scrape time or billing, so it doesn't change the timeout math above.
Deploying without a console: zip and update-function-code (AWS CLI)
zip function.zip lambda_function.py # one file, no third-party deps to bundle
aws lambda create-function --function-name sellermagnet-lookup \
--runtime python3.12 --handler lambda_function.lambda_handler \
--role arn:aws:iam::<account-id>:role/<lambda-exec-role> \
--zip-file fileb://function.zip --timeout 130
aws lambda update-function-configuration --function-name sellermagnet-lookup \
--environment "Variables={SELLERMAGNET_API_KEY=your-key-here}"
aws lambda create-function-url-config --function-name sellermagnet-lookup --auth-type AWS_IAM
Don't put this behind API Gateway without raising the timeout
An unmodified REST or HTTP API in front of this function still enforces its own 29-second integration timeout regardless of the Lambda function's timeout setting, so a slow scrape returns an API Gateway 504 to the caller even though SellerMagnet already billed the request. Use a Function URL, or raise the integration timeout quota for a Regional API first.
Frequently Asked Questions
Does AWS Lambda's Python runtime include the requests library?
No. The managed Python 3.12 runtime preinstalls only boto3 and botocore (for AWS APIs). requests and urllib3 are not included, so a function that imports them needs its own deployment package or layer; this guide avoids that by using urllib.request from the standard library instead.
Should this function sit behind API Gateway or a Lambda Function URL?
A Function URL. API Gateway's REST and HTTP integration timeout tops out at 29 seconds and cannot be raised at all for edge-optimized APIs, but SellerMagnet's own scrape can run up to 120 seconds. A Function URL's request timeout follows the function's own configured timeout, 1 to 900 seconds.
Does a client-side timeout stop SellerMagnet from billing the request?
No. Billing happens once the scrape starts on SellerMagnet's side; a caller that gives up early (a short Lambda timeout, an API Gateway 504) has still paid for that attempt, it just never saw the answer.
What should the Lambda function's own timeout be set to?
At least 130 seconds, a margin above SellerMagnet's documented 120-second scrape allowance, and well under Lambda's own 900-second ceiling.
Where does the Lambda function read the API key from?
An environment variable (SELLERMAGNET_API_KEY), set in the function's configuration, never hardcoded in the handler - the same convention as every other quickstart in this series.
Should the Function URL's auth type be AWS_IAM or NONE?
AWS_IAM, unless the URL is meant to be called directly from a browser or an anonymous client: it requires SigV4-signed requests, so only callers with permission on the function's resource policy can invoke it. NONE makes the URL public to anyone who has it.
Bottom line: skip requests in Lambda, use urllib.request and a single file, read the key from an environment variable, and put the function behind a Function URL with its own timeout set above SellerMagnet's 120-second scrape allowance, not behind API Gateway's fixed 29-second one. The same retry policy is covered for every runtime in the Python quickstart and the rate limits and retries guide.