Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use OpenSea’s authenticated API—not browser scraping—to collect NFT metadata and marketplace listings with Python. Create an API key, send it in the x-api-key header, call the documented metadata or listing endpoint, and follow cursor pagination for list results. OpenSea’s Terms restrict unauthorized automated extraction, so check the current Terms and developer policies before collecting data at scale.

Choose the right way to collect OpenSea data

OpenSea’s API is documented for NFT, token, collection, listing, offer, and event data across supported blockchains. It provides structured responses and rate-limit headers, making it more appropriate for a data pipeline than parsing pages designed for a browser.

Approach Best for Trade-offs
REST API Metadata lookups, listing snapshots, and other request-and-response jobs You need an API key, must stay within rate limits, and need to paginate list results.
Stream API over WebSocket Monitoring live listings, sales, transfers, metadata updates, or cancellations It takes more event-handling work than a snapshot request. Persist event identifiers or timestamps so your application can deduplicate events and recover its own processing state.
Browser automation Only an authorized task that genuinely requires browser-rendered content It is less structured and may be unstable; OpenSea’s Terms prohibit unauthorized automated extraction and circumventing controls.

Use REST when you need the state available at query time. Use the Stream API when you need to react to changes as events arrive. Streamed events do not count toward API rate limits, according to OpenSea’s Stream API documentation, but you still need to design for reconnects, duplicate delivery, and durable event processing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Get an API key and configure Python

Create an API key through OpenSea’s developer flow. Treat it like a password: keep it on the server, load it from an environment variable, and never commit it to source control or put it in browser-side code. Each request must send the key in the x-api-key header.

Install the HTTP client:

python -m pip install requests

Set these environment variables in your shell or deployment environment. Copy the API base URL from the current OpenSea developer documentation; it is intentionally not guessed here. For OPENSEA_API_BASE, use the documented API base without a trailing slash. For OPENSEA_API_KEY, use your private key.

export OPENSEA_API_BASE='<API base from OpenSea developer documentation>'
export OPENSEA_API_KEY='<your private API key>'
export OPENSEA_CHAIN='ethereum'
export OPENSEA_CONTRACT='<contract address>'
export OPENSEA_TOKEN_ID='<token ID>'

The angle-bracket values are configuration to replace, not literal credentials or endpoint values. Use the chain identifier and contract address required by the current endpoint documentation for the NFT you are querying.

Fetch NFT metadata by chain, contract, and token ID

The documented metadata route pattern is /api/v2/metadata/{chain}/{contractAddress}/{tokenId}. A response can include a name, description, image, animation URL, external link, and traits. Nullable fields are normal: preserve a missing value as null or an empty database field rather than assuming every NFT supplies every property.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This Python example sends the required header, applies a timeout, reports HTTP errors distinctly, and prints normalized metadata plus one row per trait. The base URL comes from the environment so it can match the current official documentation.

import json
import os
import sys
from urllib.parse import quote

import requests

API_BASE = os.environ.get("OPENSEA_API_BASE", "").rstrip("/")
API_KEY = os.environ.get("OPENSEA_API_KEY")
CHAIN = os.environ.get("OPENSEA_CHAIN")
CONTRACT = os.environ.get("OPENSEA_CONTRACT")
TOKEN_ID = os.environ.get("OPENSEA_TOKEN_ID")

if not all((API_BASE, API_KEY, CHAIN, CONTRACT, TOKEN_ID)):
    sys.exit("Set OPENSEA_API_BASE, OPENSEA_API_KEY, OPENSEA_CHAIN, OPENSEA_CONTRACT, and OPENSEA_TOKEN_ID.")

path = "/api/v2/metadata/{}/{}/{}".format(
    quote(CHAIN, safe=""), quote(CONTRACT, safe=""), quote(TOKEN_ID, safe="")
)
response = requests.get(
    API_BASE + path,
    headers={"x-api-key": API_KEY, "Accept": "application/json"},
    timeout=(10, 30),
)

if response.status_code == 404:
    sys.exit("Metadata was not found at this route. Check the chain, contract, token ID, and endpoint documentation.")
if response.status_code in (401, 403):
    sys.exit("Authentication or authorization failed. Check the API key and its access.")
if response.status_code == 429:
    sys.exit("Rate limited. Wait according to Retry-After before retrying.")
response.raise_for_status()
data = response.json()

metadata = {
    "name": data.get("name"),
    "description": data.get("description"),
    "image": data.get("image"),
    "animation_url": data.get("animation_url"),
    "external_link": data.get("external_link"),
}
traits = [
    {"trait_type": item.get("trait_type"), "value": item.get("value")}
    for item in (data.get("traits") or [])
]
print(json.dumps({"metadata": metadata, "traits": traits}, ensure_ascii=False, indent=2))

For storage, keep metadata fields and traits in separate related records or write traits as a repeatable array. This avoids losing multiple traits by forcing them into a single column. Record the chain, contract, and token ID alongside the response so records remain identifiable even if a display name is missing or repeated.

Fetch listings with the documented endpoint and cursor

Listings are marketplace orders, not NFT metadata. Choose the current documented collection- or NFT-listing endpoint that matches your query, then request only the fields the job needs. The current OpenSea API documentation specifies that these documented endpoints exist but does not establish their exact route or parameter names; copy those from the current OpenSea API documentation rather than guessing a path.

The following reusable client handles a documented list endpoint once you supply its exact relative path and supported query parameters. It yields each result and continues with the response cursor until the API returns no cursor. Save the cursor after each successfully persisted page so an interrupted job can resume without restarting from the first page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import os
import time
from urllib.parse import urljoin

import requests

API_BASE = os.environ["OPENSEA_API_BASE"].rstrip("/")
API_KEY = os.environ["OPENSEA_API_KEY"]

session = requests.Session()
session.headers.update({"x-api-key": API_KEY, "Accept": "application/json"})


def get_json(path, params=None, attempts=5):
    url = urljoin(API_BASE + "/", path.lstrip("/"))
    for attempt in range(attempts):
        response = session.get(url, params=params, timeout=(10, 30))
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            reset = response.headers.get("X-RateLimit-Reset")
            try:
                delay = float(retry_after) if retry_after else max(1, float(reset) - time.time())
            except ValueError:
                delay = 2 ** attempt
            time.sleep(min(max(delay, 1), 300))
            continue
        if 500 <= response.status_code <= 599 and attempt + 1 < attempts:
            time.sleep(min(2 ** attempt, 30))
            continue
        response.raise_for_status()
        return response.json(), response.headers
    raise RuntimeError("Retry limit reached")


def iter_pages(listing_path, initial_params=None, saved_cursor=None):
    params = dict(initial_params or {})
    if saved_cursor:
        params["cursor"] = saved_cursor
    while True:
        payload, headers = get_json(listing_path, params)
        # Adapt these response keys to the schema documented for this endpoint.
        items = payload.get("listings", [])
        next_cursor = payload.get("next")
        yield items, next_cursor, headers
        if not next_cursor:
            break
        params = dict(initial_params or {})
        params["cursor"] = next_cursor

# Set listing_path and initial_params to the exact documented listing route
# and filters for your collection or NFT before running this job.
# Persist each page's items, then checkpoint next_cursor before fetching again.

The example marks the response keys that must be matched to the chosen endpoint’s documented schema. Cursor names, result arrays, filters, and page-size parameters can vary by endpoint; do not assume a metadata response has the same shape as a listing response. When a page is stored successfully, checkpoint its returned cursor with the job state. On restart, pass that cursor as saved_cursor.

Control rate limits and make jobs resumable

Read the X-RateLimit-* response headers and honor Retry-After on HTTP 429. OpenSea’s API Keys documentation says to wait for the duration in Retry-After before retrying. If that header is absent, use the documented reset header when available, then a bounded backoff; do not retry in a tight loop.

OpenSea gives 600 read requests per hour and 30 write requests per hour as an example response for an instant free-tier key in 2026. The same documentation says those example keys expire after seven days and limits can change. These are not durable guarantees for every key: inspect the headers returned to your own client and check your current key’s terms.

  • Cache relatively stable collection metadata and traits instead of fetching them repeatedly.
  • Batch identifiers where a documented endpoint supports batching; this can lower request count, though a larger payload may make a failed batch harder to isolate.
  • Use smaller filtered requests when practical. They are easier to retry and validate than broad jobs that fetch fields you will discard.
  • Persist both output and cursor checkpoints. Advance a checkpoint only after the corresponding page has been safely written.
  • Make writes idempotent, for example by keying records on chain, contract, token ID, and the relevant listing identifier, so a retry does not create duplicates.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Use the Stream API for live changes

If the goal is a continuously updated view of listings, sales, transfers, metadata changes, or cancellations, use OpenSea’s Stream API channels over WebSocket rather than repeatedly polling REST. Streamed events do not count toward API rate limits, as stated in OpenSea’s Stream API documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A stream is an event feed, not automatically a complete historical snapshot or a replacement for your database. Persist events before acknowledging your own processing, track event IDs or timestamps for deduplication, and define a reconnect and catch-up strategy. Use REST when you need to query a current snapshot or fill gaps your event consumer detects.

Interpret errors without confusing them with missing data

  • 404: The requested resource may not exist at that endpoint. Check the chain, contract, token ID, and whether the route is the correct metadata or listing route. Do not treat every 404 as a listing expiration without checking endpoint semantics.
  • 401 or 403: Check that the key is present, valid, and authorized for the request. Confirm the header is spelled x-api-key and was not accidentally exposed or replaced.
  • 429: The request was rate limited. Stop immediate retries, honor Retry-After, inspect rate-limit headers, and reduce request frequency or reuse cached data.
  • 5xx: A server-side failure may be transient. Retry a limited number of times with bounded exponential backoff; preserve the cursor and avoid duplicating writes.
  • Unexpectedly empty results: Verify filters and endpoint selection, then inspect the raw response and pagination cursor. An empty page is not proof that a whole collection has no listings if more pages remain.
  • Malformed or missing fields: Treat nullable fields as optional, and validate the response against the schema for that particular endpoint before flattening it into a table.

Follow OpenSea’s terms and attribution requirements

OpenSea’s Terms of Service, last updated August 27, 2026, state that scrapers, bots, and crawlers may not access, extract, or manipulate platform data without authorization. They also prohibit circumventing access controls or rate limits, sharing API keys or API data, and commercializing API data without OpenSea’s express written permission. Check the current Terms and developer policies before a large collection job or any commercial use; requirements can change.

When displaying NFTs, link back to OpenSea and preserve required attribution. Keep API keys private and do not redistribute API data or assume that having a key grants permission for every downstream use.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server, not an OpenSea metadata or listings API. It cannot replace the Python workflow above or return NFT records. If you separately need a visual capture of a public web page, one request can capture it; see the ScreenshotNeo documentation for parameters and response details.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://opensea.io -o shot.webp

For that separate screenshot use case, ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. These benefits apply to screenshot captures, not OpenSea API requests or NFT data collection.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card required.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.