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.
| Credential | Header | Required for |
|---|---|---|
| API Key | x-api-key | All requests |
| Access Token | Authorization: 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:
| Step | Token | How you get it | What it's for |
|---|---|---|---|
| 1 | Session token | SIWE sign-in (/api/v2/auth/siwe/verify) | HTTP-only cookie; authenticates scoped-token management only |
| 2 | Scoped token | POST /api/v2/auth/tokens | Opaque string; selects which scopes to grant |
| 3 | Access token | POST /api/v2/auth/tokens/exchange | JWT; 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 statusUse 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.
| Scope | What it allows | MCP tool |
|---|---|---|
read:eligibility | Check whether the signed-in wallet can mint a drop | check_drop_eligibility |
read:favorites | Read NFT favorites and token or perpetual watchlists | get_favorites |
write:favorites | Add or remove watchlist entries | manage_watchlist |
write:orders | Cancel an order for the signed-in wallet | cancel_orders |
write:drops | Edit Creator Studio drops and their items | manage_drops |
write:collections | Edit collection metadata, visibility, and images | manage_collections |
write:profile | Edit the profile, username, image, and shelves | manage_profile |
write:wallets | Link or unlink wallets on the account | manage_wallets |
The API reference pages below contain the request fields, response schemas, and examples for each operation.
read:eligibility
read:eligibility| Method | API reference | What it does |
|---|---|---|
GET | Get drop eligibility | Returns eligibility, price, and mint limits for each stage. |
read:favorites
read:favoritesThe account in the path must belong to the access token. A different account returns 403.
| Method | API reference | What it does |
|---|---|---|
GET | Get profile favorites | Lists NFTs favorited by the account. |
GET | Get token watchlist | Lists fungible tokens watched by the account. |
GET | Get perpetual watchlist | Lists perpetual markets watched by the account. |
write:favorites
write:favorites| Method | API reference | What it does |
|---|---|---|
POST | Add a watchlist entry | Adds an NFT collection, fungible token, or perpetual market. |
DELETE | Remove a watchlist entry | Removes an NFT collection, fungible token, or perpetual market. |
write:orders
write:orders| Method | API reference | What it does |
|---|---|---|
POST | Cancel an order | Cancels one order. The Bearer-token flow requires write:orders; the legacy signed cancellation flow remains available with an API key. |
write:drops
write:dropsThese operations edit Creator Studio state or return transaction data for the signed-in creator. They do not broadcast a transaction for you.
| Method | API reference | What it does |
|---|---|---|
POST | Update drop edits | Saves drop settings and stage edits. |
POST | Create a SelfMint drop item | Builds transaction data for a new SelfMint item. |
POST | Upload a drop allowlist | Returns short-lived storage upload context for the allowlist file. |
POST | Validate a drop allowlist | Validates the token returned after the storage upload succeeds. |
PUT | Update a SelfMint drop item | Builds transaction data for an existing SelfMint item. |
POST | Save a prereveal item | Saves prereveal metadata for the drop. |
POST | Upload drop item media | Returns short-lived storage upload context for each filename. |
POST | Save drop item media | Saves the media tokens after every storage upload succeeds. |
PATCH | Update a drop item | Updates an item's offchain metadata. |
write:collections
write:collectionsThe signed-in account must own the collection. An authenticated non-owner receives 403.
| Method | API reference | What it does |
|---|---|---|
PATCH | Modify collection metadata | Updates the collection fields accepted by the combined editor. |
PATCH | Update collection metadata | Updates metadata fields through the dedicated metadata endpoint. |
PATCH | Set collection visibility | Changes collection visibility settings. |
POST | Upload a collection image | Returns short-lived storage upload context for a logo, banner, or featured image. |
write:profile
write:profile| Method | API reference | What it does |
|---|---|---|
PATCH | Update profile settings | Updates profile fields such as name, bio, and links. |
POST | Claim a profile username | Claims or changes the account username. |
POST | Upload a profile image | Returns short-lived storage upload context for a profile or banner image. |
POST | Create a profile shelf | Creates a shelf on the public profile. |
PATCH | Reorder profile shelves | Changes the order of profile shelves. |
PATCH | Update a profile shelf | Changes a shelf's title or contents. |
DELETE | Delete a profile shelf | Removes a profile shelf. |
Get profile shelves is read-only and needs an API key, but not a wallet access token.
write:wallets
write:wallets| Method | API reference | What it does |
|---|---|---|
POST | Link a wallet | Verifies a SIWX signature and links the wallet to the account. |
DELETE | Unlink a wallet | Removes 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
| Token | Lifetime | Notes |
|---|---|---|
| Session token (cookie) | 3.5 days access / 30 days refresh | Authenticates to auth endpoints only |
| Scoped token | Set at creation (expiresInDays) | Opaque; used as subject token for exchange |
| Access token (JWT) | ~12 hours | Bearer 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.txtEach 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:
| Method | Path | Description |
|---|---|---|
GET | /api/v2/auth/tokens | List your tokens |
POST | /api/v2/auth/tokens | Create a token (label, scopes, expiresInDays) |
DELETE | /api/v2/auth/tokens/{id} | Revoke a token |
POST | /api/v2/auth/tokens/{id}/rotate | Rotate (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:
- SIWE sign-in with a programmatic signer (ethers.js
Walletor similar) - Create a scoped token with the needed scopes
- Exchange for an access JWT
- Pass the JWT as
Authorization: Bearerwhen 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.
| Status | Message | Meaning |
|---|---|---|
400 | Bad request | Invalid SIWE message format or signature |
401 | Invalid or expired token | Access token is missing, expired, malformed, or invalid |
401 | Wallet is no longer authorized | Wallet or account is blocked |
403 | Insufficient permissions | Token lacks the required scope |
403 | Wallet is in cooldown period | Wallet is in a cooldown window for a write action |
429 | Rate limit exceeded | Too 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
