PSA Card APIpsacard.com ↗
Access PSA certification details, population reports, and price guide data for graded trading cards and collectibles via a structured REST API.
What is the PSA Card API?
This API provides 5 endpoints covering PSA-graded collectible data, including certification lookups, population report searches, and price guide queries. The get_cert_details endpoint returns card name, grade, images, and population counts for any PSA certification number, while search_population_report and get_population_report_by_set expose grading distribution data across all grade levels (1–10) for every tracked card set.
curl -X GET 'https://api.parse.bot/scraper/311daf8c-242f-4c68-af70-b50617fd1d13/get_cert_details?cert_number=12345678' \ -H 'X-API-Key: $PARSE_API_KEY'
Typed, relational, agent-ready
A generated client with real types, enums, and the links between objects — the structure a flat JSON response can't carry. Autocompletes in your editor and reads cleanly to coding agents.
- Fully typed · autocompletes
- Objects link to objects
- Typed errors & pagination
Typed Python client. Set up the SDK in your uv project, then pull this API’s typed client:
uv add parse-sdk uv run parse init uv run parse add --marketplace psacard-com-api
uv run parse add --marketplace pulls a pinned snapshot of this canonical API — it won’t change underneath you. To customize it, subscribe and swap to your own copy.
"""
PSA Card API Client
Get your API key from: https://parse.bot/settings
"""
import os
import requests
from typing import Optional, Dict, Any, List
class ParseClient:
"""Client for interacting with the PSA Card API via Parse Bot."""
def __init__(self, api_key: Optional[str] = None):
"""
Initialize the Parse API client.
Args:
api_key: API key from https://parse.bot/settings
Falls back to PARSE_API_KEY environment variable
"""
self.base_url = "https://api.parse.bot"
self.scraper_id = "311daf8c-242f-4c68-af70-b50617fd1d13"
self.api_key = api_key or os.getenv("PARSE_API_KEY")
if not self.api_key:
raise ValueError("API key required. Pass api_key or set PARSE_API_KEY env var")
def _call(self, endpoint: str, method: str = "POST", **params) -> Dict[str, Any]:
"""
Make an API call to the Parse Bot scraper.
Args:
endpoint: API endpoint name
method: HTTP method (GET or POST)
**params: Query/body parameters
Returns:
Response JSON as dictionary
"""
url = f"{self.base_url}/scraper/{self.scraper_id}/{endpoint}"
headers = {
"X-API-Key": self.api_key,
"Content-Type": "application/json"
}
if method.upper() == "GET":
response = requests.get(url, headers=headers, params=params)
else:
response = requests.post(url, headers=headers, json=params)
response.raise_for_status()
return response.json()
def get_cert_details(self, cert_number: str) -> Dict[str, Any]:
"""
Retrieve details for a specific PSA certification number.
Args:
cert_number: PSA Certification Number (e.g., 69225215)
Returns:
Dictionary containing card_title, grade, population, images, etc.
"""
return self._call("get_cert_details", method="GET", cert_number=cert_number)
def search_population_report(
self,
query: str,
category_id: int = 0
) -> Dict[str, Any]:
"""
Search the PSA Population Report by keyword.
Args:
query: Search keyword (e.g., set name or player name)
category_id: Optional Category ID filter (e.g., 156940 for TCG)
Returns:
Dictionary with success flag and data array of matching sets
"""
return self._call(
"search_population_report",
method="GET",
query=query,
category_id=category_id
)
def get_population_report_by_set(
self,
set_id: str,
category_id: str,
start: int = 0,
length: int = 300,
search: str = ""
) -> Dict[str, Any]:
"""
Retrieve the full population report for a specific card set.
Args:
set_id: Heading ID for the set (e.g., 57801)
category_id: Category ID (e.g., 156940)
start: Pagination offset
length: Number of records to return
search: Search filter within the set
Returns:
Dictionary with population data for all cards in the set
"""
return self._call(
"get_population_report_by_set",
method="GET",
set_id=set_id,
category_id=category_id,
start=start,
length=length,
search=search
)
def search_price_guide(
self,
query: str,
category_id: int = 0
) -> Dict[str, Any]:
"""
Search the PSA Price Guide by keyword.
Args:
query: Search keyword
category_id: Optional Category ID filter
Returns:
Dictionary with success flag and data array of matching items
"""
return self._call(
"search_price_guide",
method="GET",
query=query,
category_id=category_id
)
def browse_population_categories(self) -> Dict[str, Any]:
"""
Browse available population report categories.
Returns:
Dictionary with categories array containing name, url, and slug
"""
return self._call("browse_population_categories", method="GET")
if __name__ == "__main__":
client = ParseClient()
print("=" * 80)
print("PSA Card API - Practical Workflow: Pokemon Card Population Analysis")
print("=" * 80)
# Step 1: Browse categories to understand the data structure
print("\n[Step 1] Browsing available collectible categories...")
categories_resp = client.browse_population_categories()
categories = categories_resp.get("categories", [])
print(f"✓ Found {len(categories)} categories")
tcg_category = next((c for c in categories if c.get("slug") == "tcg-cards"), None)
if tcg_category:
tcg_id = tcg_category["category_id"]
print(f" → Using TCG category ID: {tcg_id}")
# Step 2: Search for a specific card set
print("\n[Step 2] Searching for 'Pokemon Base Set' in population report...")
search_resp = client.search_population_report(
query="Pokemon Base Set",
category_id=156940
)
if search_resp.get("success") and search_resp.get("data"):
sets = search_resp["data"]
print(f"✓ Found {search_resp['recordsTotal']} matching results")
# Filter for the exact Base Set
base_set = next(
(s for s in sets if "base" in s["SetName"].lower() and "1999" in s["YearIssued"]),
sets[0]
)
set_id = str(base_set["HeadingID"])
set_name = base_set["SetName"]
year = base_set["YearIssued"]
category_id = str(base_set["SportCategoryID"])
print(f" → Selected: {set_name} ({year})")
print(f" → Set ID: {set_id}, Category ID: {category_id}")
# Step 3: Get population data for this set
print(f"\n[Step 3] Retrieving population data for '{set_name}'...")
pop_resp = client.get_population_report_by_set(
set_id=set_id,
category_id=category_id,
start=0,
length=50
)
if pop_resp.get("data"):
cards = pop_resp["data"]
total_cards = pop_resp.get("recordsTotal", 0)
print(f"✓ Retrieved {len(cards)} cards (total in set: {total_cards})")
# Step 4: Analyze population statistics
print(f"\n[Step 4] Analyzing top 5 most graded cards by population...")
print("-" * 80)
# Sort by total population
top_cards = sorted(
cards,
key=lambda x: int(x.get("Total", 0)),
reverse=True
)[:5]
for idx, card in enumerate(top_cards, 1):
subject = card.get("SubjectName", "Unknown")
card_num = card.get("CardNumber", "?")
total = int(card.get("Total", 0))
grade_10 = int(card.get("Grade10", 0))
grade_9 = int(card.get("Grade9", 0))
grade_8 = int(card.get("Grade8", 0))
gem_mint_pct = (grade_10 / total * 100) if total > 0 else 0
mint_pct = (grade_9 / total * 100) if total > 0 else 0
print(f"\n{idx}. {subject} (Card #{card_num})")
print(f" Total Graded: {total:,}")
print(f" Grade 10 (Gem Mint): {grade_10:,} ({gem_mint_pct:.1f}%)")
print(f" Grade 9 (Mint): {grade_9:,} ({mint_pct:.1f}%)")
print(f" Grade 8 (VF-NM): {grade_8:,}")
# Step 5: Search price guide for this set
print(f"\n[Step 5] Searching PSA Price Guide for '{set_name}'...")
price_resp = client.search_price_guide(
query=set_name,
category_id=156940
)
if price_resp.get("success") and price_resp.get("data"):
price_entries = price_resp["data"]
print(f"✓ Found {price_resp['recordsTotal']} price guide entries")
# Show top matching price entries
print("\nTop 3 matching price guide entries:")
for idx, entry in enumerate(price_entries[:3], 1):
entry_name = entry.get("SetName", "Unknown")
entry_category = entry.get("CategoryName", "N/A")
entry_url = entry.get("url", "N/A")
print(f" {idx}. {entry_name}")
print(f" Category: {entry_category}")
# Step 6: Find a specific high-value card and get cert details
print(f"\n[Step 6] Example: Getting certification details...")
print("-" * 80)
# For demonstration, use a known cert number (from API spec example)
example_cert = "69225215"
print(f"Retrieving details for certification #{example_cert}...")
try:
cert_resp = client.get_cert_details(cert_number=example_cert)
if cert_resp.get("status") == "success":
cert_data = cert_resp.get("data", {})
cert_num = cert_data.get("cert_number", "Unknown")
card_title = cert_data.get("card_title", "Unknown")
grade = cert_data.get("grade", "Unknown")
population = cert_data.get("population", "N/A")
print(f"✓ Cert #{cert_num}")
print(f" Card: {card_title}")
print(f" Grade: {grade}")
print(f" Population: {population}")
images = cert_data.get("images", [])
if images:
print(f" Images: {len(images)} available")
else:
print(" (Example cert details would appear here)")
except Exception as e:
print(f" (Demo mode - cert lookup may be rate-limited or require direct lookup)")
print("\n" + "=" * 80)
print("Workflow complete! You now have:")
print(" • Population statistics for the set")
print(" • Grade distribution analysis")
print(" • Price guide references")
print(" • Individual certification details")
print("=" * 80)Retrieve details for a specific PSA certification number. Returns card name, grade, population data, and images. Note: This endpoint may be subject to Cloudflare challenges.
| Param | Type | Description |
|---|---|---|
| cert_numberrequired | string | PSA Certification Number (e.g., '12345678') |
{
"type": "object",
"fields": {
"year": "string",
"brand": "string",
"grade": "string",
"images": "array",
"subject": "string",
"card_title": "string",
"population": "string",
"cert_number": "string"
},
"sample": {
"data": {
"url": "https://www.psacard.com/cert/69225215/psa",
"cert_number": "69225215"
},
"status": "success"
}
}About the PSA Card API
Certification Lookups and Population Data
The get_cert_details endpoint accepts a PSA certification number (e.g., '12345678') and returns structured data including year, brand, subject, card_title, grade, population, cert_number, and any associated images. This is the primary way to verify a single graded card's details. Note that this endpoint may encounter Cloudflare challenges, which can affect response availability.
Population Report Search and Set-Level Data
search_population_report takes a query string — such as a set name like '1999 Pokemon Game' or a player name like 'Mickey Mantle' — and returns an array of matching card sets. Each result includes HeadingID, SetName, YearIssued, SportCategoryID, and CategoryName. The HeadingID and SportCategoryID values feed directly into get_population_report_by_set, which returns per-card grading counts (Grade1 through Grade10) alongside SubjectName, CardNumber, SpecID, and Variety. Pagination is supported via start and length parameters, and an optional search string filters results within a set.
Price Guide and Category Browsing
search_price_guide accepts the same query and optional category_id inputs as the population search, returning matching entries with SMRSetID, SetName, SportCategoryID, CategoryName, and SetNameSEO. For both price guide and population searches, setting category_id to 0 searches across all categories — non-zero values may return empty results. browse_population_categories requires no inputs and returns the full list of collectible categories, each with name, url, slug, and category_id, which can be used to understand available scopes across the other endpoints.
The PSA Card API is a managed, monitored endpoint for psacard.com — not a raw scraper you maintain. Every endpoint is automatically health-checked on a schedule, and when psacard.com changes and a check fails, the API is automatically queued for repair and re-verified. It is built to keep working as the site underneath it changes.
This isn't an official psacard.com API — it's an independent, maintained REST wrapper over public data. Where the source has no official API (or only a limited one), Parse gives you a stable contract over a source that never promised one, and keeps it current. Need a new endpoint or field? You can revise it yourself in plain English and the agent rebuilds it against the live site in minutes — contributing the change back to the shared API is free.
Will this API break when the source site changes?+
Is this an official API from the source site?+
Can I fix or extend this API myself if I need a new endpoint or field?+
What happens if I call an endpoint that has an issue?+
- Verify authenticity and grade of a PSA-certified card using its certification number via get_cert_details
- Build a collection tracker that pulls grade-level population counts for specific card sets using get_population_report_by_set
- Compare population scarcity across PSA 9 and PSA 10 grades to inform buying and selling decisions
- Search price guide entries for a specific player or set to surface SMRSetID links for pricing research
- Enumerate all collectible categories via browse_population_categories to build a structured navigation or filtering UI
- Monitor population changes for vintage baseball or Pokemon card sets by periodically querying search_population_report
- Cross-reference HeadingID values from population searches to retrieve full grading distributions for investment analysis
| Tier | Price | Credits/month | Rate limit |
|---|---|---|---|
| Free | $0/mo | 100 | 5 req/min |
| Hobby | $30/mo | 1,000 | 20 req/min |
| Developer | $100/mo | 5,000 | 100 req/min |
One credit = one API call regardless of which marketplace API you call. Exceeding the rate limit returns a 429 response. Authenticate with the X-API-Key header.
Does PSA have an official developer API?+
What exactly does get_population_report_by_set return, and how do I get the inputs for it?+
SpecID, SubjectName, CardNumber, Variety, and numeric grade counts from Grade1 through Grade10. You obtain the required set_id from the HeadingID field and category_id from the SportCategoryID field returned by search_population_report. Pagination is controlled via start and length.Does the API return actual sale prices or price history for graded cards?+
search_price_guide endpoint returns set-level metadata including SMRSetID, SetName, and SetNameSEO, but does not return individual card prices or historical price data. You can fork this API on Parse and revise it to add an endpoint targeting per-card price data from the PSA Price Guide.Are there any known limitations with the get_cert_details endpoint?+
Can I filter population report searches by category to narrow results?+
category_id parameter is available on both search_population_report and search_price_guide, but non-zero values may return empty results. The reliable approach is to use category_id: 0 (the default) to search across all categories, then use the CategoryName and SportCategoryID fields in the response to identify the right category for downstream filtering with get_population_report_by_set.