Authentication

🚧

Beta: Access tokens are rolling out gradually and may not be available on your account yet. If you hit an error, please check back soon.

The OpenSea API uses API keys to identify your application and access tokens to identify your wallet for wallet-specific endpoints.

CredentialHeaderRequired for
API Keyx-api-keyAll requests
Access TokenAuthorization: Bearer <token>Wallet-specific endpoints (drops, favorites, order cancellation)

Public endpoints (collections, NFTs, stats, events) require only an API key. See Getting Your API Key.

How authentication works

Interactive clients should use the standard OAuth 2.1 authorization-code flow implemented by opensea login and compatible MCP clients. The manual SIWE flow below is available for server-side agents that control a signing key. It involves three tokens:

StepTokenHow you get itWhat it's for
1Session tokenSIWE sign-in (/api/v2/auth/siwe/verify)HTTP-only cookie; authenticates scoped-token management only
2Scoped tokenPOST /api/v2/auth/tokensOpaque string; selects which scopes to grant
3Access tokenPOST /api/v2/auth/tokens/exchangeJWT; this is the Bearer token for api.opensea.io

All auth endpoints are available at https://api.opensea.io.

Sign in with the CLI

For local development, the CLI handles OAuth, opens the browser, and stores the resulting access and refresh tokens in ~/.opensea/auth.json:

npm install -g @opensea/cli
opensea login --scopes read:favorites,write:profile
opensea auth status

Use opensea login --device on a headless or remote machine. Other CLI commands read the stored credentials automatically. The manual flow below is useful when you need to build authentication into your own app.

Step 1: Sign in with SIWE

SIWE (Sign-In with Ethereum) proves wallet ownership by signing a message.

1a. Get a nonce:

curl -X POST "https://api.opensea.io/api/v2/auth/siwe/nonce" -H "Content-Type: application/json"
# Response: { "nonce": "a1b2c3d4e5f6g7h8" }

1b. Sign a SIWE message:

import { SiweMessage } from "siwe";
import { ethers } from "ethers";

const signer = await new ethers.BrowserProvider(window.ethereum).getSigner();

const message = new SiweMessage({
  domain: "opensea.io",
  address: await signer.getAddress(),
  statement:
    "Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).",
  uri: "https://opensea.io",
  version: "1",
  chainId: 1,
  nonce: "<nonce from 1a>",
});

const messageString = message.prepareMessage();
const signature = await signer.signMessage(messageString);

1c. Verify the signature:

curl -c cookies.txt -X POST "https://api.opensea.io/api/v2/auth/siwe/verify" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "domain": "opensea.io",
      "address": "0xYourWalletAddress",
      "statement": "Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).",
      "uri": "https://opensea.io",
      "version": "1",
      "chainId": 1,
      "nonce": "<nonce from 1a>",
      "issuedAt": "2026-01-01T00:00:00.000Z",
      "accountType": "Ethereum"
    },
    "signature": "0xYourSignature...",
    "chainArch": "EVM"
  }'

chainArch accepts "EVM" (Ethereum, Base, Polygon, etc.) or "SVM" (Solana).

accountType is required and must match the account line of the signed message: "Ethereum" for EVM chains, "Solana" for SVM. If it is missing, the server reconstructs a message without the account type and verification fails with 400 Invalid signature, even though your signature is correct. Note that the siwe library does not include this field on SiweMessage, so add it when you build the request body.

On success, the response sets access_token and refresh_token as HTTP-only cookies. These session cookies authenticate scoped-token management, but are not the Bearer token for api.opensea.io.

Step 2: Create a scoped token

With an active session (cookies from step 1), create a scoped token that grants specific API capabilities:

curl -b cookies.txt -X POST "https://api.opensea.io/api/v2/auth/tokens" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "my-app-token",
    "scopes": ["read:eligibility"],
    "expiresInDays": 30
  }'

Response:

{
  "id": "380615563040307696",
  "label": "my-app-token",
  "scopes": ["read:eligibility"],
  "createdAt": "2026-01-01T00:00:00Z",
  "expiresAt": "2026-01-31T00:00:00Z",
  "token": "5Jiw...opaque base64url string, ~95 chars..."
}

Save the token value. It is an opaque string with no recognizable prefix, and it is only returned once.

You can only grant scopes you already hold. Requested scopes must be a subset of your own effective scopes; requesting a scope you do not hold returns 403, and an unknown scope string returns 400. This keeps a token from ever granting more than its creator has.

Step 3: Exchange for an access token

Exchange the scoped token for a JWT. This request needs the scoped token from step 2, not a session cookie or another wallet signature. This is the Bearer token that api.opensea.io accepts:

curl -X POST "https://api.opensea.io/api/v2/auth/tokens/exchange" \
  -H "Content-Type: application/json" \
  -d '{
    "subjectToken": "<token value from step 2>",
    "subjectTokenType": "ACCESS_TOKEN"
  }'

Response:

{
  "accessToken": "eyJhbGciOiJSUzI1NiIs...",
  "tokenType": "Bearer",
  "expiresIn": 43200,
  "scope": "openid read:eligibility",
  "tokenScopes": ["read:eligibility"]
}

Step 4: Make authenticated requests

Use the exchanged accessToken as a Bearer token alongside your API key:

curl "https://api.opensea.io/api/v2/drops/my-drop/eligibility" \
  -H "x-api-key: your-api-key" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

Scopes

Each scoped token grants access to specific capabilities. Request only what you need.

ScopeWhat it allowsMCP tool
read:eligibilityCheck whether the signed-in wallet can mint a dropcheck_drop_eligibility
read:favoritesRead NFT favorites and token or perpetual watchlistsget_favorites
write:favoritesAdd or remove watchlist entriesmanage_watchlist
write:ordersCancel an order for the signed-in walletcancel_orders
write:dropsEdit Creator Studio drops and their itemsmanage_drops
write:collectionsEdit collection metadata, visibility, and imagesmanage_collections
write:profileEdit the profile, username, image, and shelvesmanage_profile
write:walletsLink or unlink wallets on the accountmanage_wallets

The API reference pages below contain the request fields, response schemas, and examples for each operation.

read:eligibility

MethodAPI referenceWhat it does
GETGet drop eligibilityReturns eligibility, price, and mint limits for each stage.

read:favorites

The account in the path must belong to the access token. A different account returns 403.

MethodAPI referenceWhat it does
GETGet profile favoritesLists NFTs favorited by the account.
GETGet token watchlistLists fungible tokens watched by the account.
GETGet perpetual watchlistLists perpetual markets watched by the account.

write:favorites

MethodAPI referenceWhat it does
POSTAdd a watchlist entryAdds an NFT collection, fungible token, or perpetual market.
DELETERemove a watchlist entryRemoves an NFT collection, fungible token, or perpetual market.

write:orders

MethodAPI referenceWhat it does
POSTCancel an orderCancels one order. The Bearer-token flow requires write:orders; the legacy signed cancellation flow remains available with an API key.

write:drops

These operations edit Creator Studio state or return transaction data for the signed-in creator. They do not broadcast a transaction for you.

MethodAPI referenceWhat it does
POSTUpdate drop editsSaves drop settings and stage edits.
POSTCreate a SelfMint drop itemBuilds transaction data for a new SelfMint item.
POSTUpload a drop allowlistReturns short-lived storage upload context for the allowlist file.
POSTValidate a drop allowlistValidates the token returned after the storage upload succeeds.
PUTUpdate a SelfMint drop itemBuilds transaction data for an existing SelfMint item.
POSTSave a prereveal itemSaves prereveal metadata for the drop.
POSTUpload drop item mediaReturns short-lived storage upload context for each filename.
POSTSave drop item mediaSaves the media tokens after every storage upload succeeds.
PATCHUpdate a drop itemUpdates an item's offchain metadata.

write:collections

The signed-in account must own the collection. An authenticated non-owner receives 403.

MethodAPI referenceWhat it does
PATCHModify collection metadataUpdates the collection fields accepted by the combined editor.
PATCHUpdate collection metadataUpdates metadata fields through the dedicated metadata endpoint.
PATCHSet collection visibilityChanges collection visibility settings.
POSTUpload a collection imageReturns short-lived storage upload context for a logo, banner, or featured image.

write:profile

MethodAPI referenceWhat it does
PATCHUpdate profile settingsUpdates profile fields such as name, bio, and links.
POSTClaim a profile usernameClaims or changes the account username.
POSTUpload a profile imageReturns short-lived storage upload context for a profile or banner image.
POSTCreate a profile shelfCreates a shelf on the public profile.
PATCHReorder profile shelvesChanges the order of profile shelves.
PATCHUpdate a profile shelfChanges a shelf's title or contents.
DELETEDelete a profile shelfRemoves a profile shelf.

Get profile shelves is read-only and needs an API key, but not a wallet access token.

write:wallets

MethodAPI referenceWhat it does
POSTLink a walletVerifies a SIWX signature and links the wallet to the account.
DELETEUnlink a walletRemoves a linked wallet from the account.

To fetch the current scope list programmatically, call the public discovery endpoint (no auth required):

curl "https://api.opensea.io/api/v2/auth/scopes" -H "x-api-key: your-api-key"

Each entry returns its key, displayName, description, group, and the endpoints and mcpTools it grants. This is the source of truth for what you can request, so agents can discover valid scopes without hardcoding them.

Token lifecycle

TokenLifetimeNotes
Session token (cookie)3.5 days access / 30 days refreshAuthenticates to auth endpoints only
Scoped tokenSet at creation (expiresInDays)Opaque; used as subject token for exchange
Access token (JWT)~12 hoursBearer token for api.opensea.io; re-exchange when expired

Refreshing the session: Session cookies expire after 3.5 days. Use the refresh endpoint to extend without re-signing:

curl -X POST "https://api.opensea.io/api/v2/auth/session/refresh" \
  -b "refresh_token=<your_refresh_token>" \
  -c cookies.txt

Each refresh rotates the refresh token. Replaying a previously-used refresh token does not mint another session; it idempotently returns the session already issued for it, so always store the newest cookie values. If the refresh token has expired (after 30 days), re-authenticate via SIWE.

Re-exchanging: When the access token JWT expires (~12h), call /api/v2/auth/tokens/exchange again with the same scoped token to get a fresh JWT. This does not need a session cookie or another SIWE signature. You only need to create a new scoped token if the current one expires, is revoked, or is rotated.

Token management API

Manage scoped tokens programmatically at https://api.opensea.io/api/v2/auth/tokens. These management endpoints accept either an active session (cookies from step 1) or an access-token JWT as Authorization: Bearer, so an agent can manage its own tokens without a browser session:

MethodPathDescription
GET/api/v2/auth/tokensList your tokens
POST/api/v2/auth/tokensCreate a token (label, scopes, expiresInDays)
DELETE/api/v2/auth/tokens/{id}Revoke a token
POST/api/v2/auth/tokens/{id}/rotateRotate (invalidates old, issues new)

Rotation preserves scopes. Rotate keeps the original token's exact scope set and cannot broaden it; any scopes passed to the rotate endpoint is ignored.

Exchange is sessionless. POST /api/v2/auth/tokens/exchange accepts the scoped token in its request body as its sole credential. Keep that token secret: anyone who obtains it can exchange it for a JWT until you revoke, rotate, or it expires.

Revocation is best-effort. Revoking (or rotating) a scoped token stops future exchanges, but it does not invalidate access-token JWTs that were already exchanged from it. Those keep working until they expire (~12h), so treat the access token's lifetime as the real revocation window.

AI agents and MCP

MCP clients should use OpenSea's standard OAuth flow. Start with the protected-resource metadata at https://mcp.opensea.io/.well-known/oauth-protected-resource; it identifies https://auth.opensea.io as the authorization server and lists the eight wallet scopes. A compatible MCP client then handles authorization code with PKCE, token refresh, and Bearer headers automatically.

Every MCP request also needs an OpenSea API key in X-API-KEY. Wallet-scoped tools additionally need the OAuth access token in Authorization: Bearer <token>. A missing scope returns 403.

For a non-interactive server agent that controls a signing key, automate the manual flow instead:

  1. SIWE sign-in with a programmatic signer (ethers.js Wallet or similar)
  2. Create a scoped token with the needed scopes
  3. Exchange for an access JWT
  4. Pass the JWT as Authorization: Bearer when connecting to the OpenSea MCP server

Once it holds an access-token JWT, an agent can manage its own scoped tokens with that Bearer token (list, create, rotate, revoke) instead of a browser session. New tokens it mints are still capped to the agent's own scopes, so it can hand a narrower token to a sub-task but never a broader one.

SDK helper: @opensea/sdk exports OpenSeaAuth for key-based callers, so you do not hand-roll the nonce, signature, and refresh steps:

import { OpenSeaAuth } from "@opensea/sdk";

const auth = new OpenSeaAuth();
const token = await auth.authenticate(signer, { scopes: ["read:eligibility"] });
// token.accessToken is the Bearer token for api.opensea.io.
// Call auth.getValidToken() to get a token that auto-refreshes before it expires.

For interactive use, @opensea/cli provides keyless opensea login with OAuth 2.1 and PKCE. Use opensea login --device on a remote or headless machine.

Error responses

Wallet-scoped data endpoints return errors as { "errors": ["<message>"] }. The auth endpoints (/api/v2/auth/*) use a different shape:

{
  "error": { "message": "Invalid signature", "status": "BAD_REQUEST", "code": 400 },
  "meta": { "x-trace-id": "..." }
}

Validation failures on auth endpoints return 422 with a details array listing the offending fields.

StatusMessageMeaning
400Bad requestInvalid SIWE message format or signature
401Invalid or expired tokenAccess token is missing, expired, malformed, or invalid
401Wallet is no longer authorizedWallet or account is blocked
403Insufficient permissionsToken lacks the required scope
403Wallet is in cooldown periodWallet is in a cooldown window for a write action
429Rate limit exceededToo many requests; check the Retry-After header

Security best practices

  • Minimum scopes: only request what your app needs
  • Keep credentials secret: never share or expose private keys, session cookies, scoped tokens, access JWTs, refresh tokens, SIWE signatures, or full Authorization headers in client-side code, logs, or version control
  • Rotate periodically and revoke immediately if compromised
  • Secure signing keys: use env vars or a secrets manager, never hardcode private keys
  • Separate tokens per environment (dev, staging, prod)
  • Use a throwaway wallet for testing and keep its permissions and balances minimal
  • Prefer reversible writes in QA and restore modified profile, collection, drop, and favorite data afterward
  • Require explicit human confirmation before broadcasting a mainnet transaction, approving token spend, or signing a spend-bearing payload