Home / Blog / Amazon Product API from an AWS Lambda Function (No Dependencies)

Amazon Product API from an AWS Lambda Function (No Dependencies)

Call the SellerMagnet API from a single-file AWS Lambda function with no deployment package: urllib.request from the standard library instead of requests (the Python runtime ships boto3 only), exposed through a Lambda Function URL instead of API Gateway because the API's own 120-second scrape allowance is longer than API Gateway's fixed 29-second integration timeout.

November 10, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Terminal showing an AWS Lambda function invoking the SellerMagnet API through urllib

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)
Blueprint comparing API Gateway's fixed 29-second integration timeout against a Lambda Function URL's configurable 1 to 900 second timeout
SellerMagnet's scrape can take up to 120 seconds; only the Function URL path can wait that long.

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.

SellerMagnet status codes, billing and the Lambda handler's reaction.
StatusMeaningChargedHandler action
200successYesUse data
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
Blueprint flow diagram from a client request through the Lambda Function URL to the SellerMagnet API and back
A malformed ASIN never leaves the function; every call that does reach SellerMagnet is billed, retries included.

How do I deploy and configure it?

  • Lambda console -> Create function -> Author from scratch -> Runtime: Python 3.12.
  • Paste lambda_function.py into the inline editor (no requirements.txt, no layer).
  • Configuration -> Environment variables -> add SELLERMAGNET_API_KEY with 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.

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