# PSA Card Cert Lookup - Grade, Population & Sales (`fanndev/psa-card-cert-lookup-scraper`) Actor

Look up PSA graded cards by certification number: item details and grade, the population at that grade, how many copies grade higher, PSA's own value estimate, and recent comparable sales with prices, dates and marketplaces. No account needed.

- **URL**: https://apify.com/fanndev/psa-card-cert-lookup-scraper.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Categories:** E-commerce, Other
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PSA Card Cert Lookup - Grade, Population & Comparable Sales

Turn a PSA certification number into a complete row: what the card is, what grade it earned, **how many exist at that grade**, how many grade higher, PSA's own value estimate, and the recent sales of comparable copies with prices, dates and marketplaces.

Population tells you how rare a card is. Sales tell you what it is worth. This Actor returns both from the same lookup - no PSA account, no API key.

### Why use this actor

- **Population and price in one row.** Most tools give you one or the other; the scarcity number and the market comps arrive together, so a rarity-versus-value screen is a single sort.
- **Real comparable sales** - price, date, marketplace, sale type, the grade that sold and its own certificate number, so you can follow any comp back to its own record.
- **`Pop Higher = 0` is the collector's question** - it tells you instantly whether a card is the top-graded copy.
- **Bulk-friendly.** Feed a list of certificate numbers, or walk a consecutive block with `range` mode, since PSA issues certificates sequentially.
- **Fails loudly.** An unknown certificate returns `not_found`, junk input returns `invalid_cert`, and a layout change returns `shape_changed` instead of a silently blank row.

### How it works

1. You give the Actor one or more PSA certification numbers - the number printed on the label. Full `psacard.com/cert/...` URLs and `#`-prefixed numbers work too.
2. The Actor opens each public verification page and reads everything it publishes.
3. `cert` mode returns one row per certificate with the item, its population and a sales summary; `sales` mode returns one row per comparable sale.
4. Rows stream into your dataset for JSON, CSV or Excel export.

### Input

```json
{
    "mode": "cert",
    "certNumbers": ["49204884"],
    "maxItems": 200,
    "maxConcurrency": 3
}
```

| Field | Type | Description |
|---|---|---|
| `mode` | string | `cert`, `sales`, `range` or `reference`. |
| `certNumber` / `certNumbers` | string / array | Certification numbers, URLs or `#`-prefixed numbers. |
| `startCert` | string | `range` mode: first certificate of the block. |
| `rangeCount` | integer | `range` mode: how many consecutive certificates to walk (max 5000). |
| `maxItems` | integer | Row cap for the run. Default `200`. |
| `maxConcurrency` | integer | Parallel lookups. Default `3` - keep it modest. |
| `proxyConfiguration` | object | **Residential proxy, on by default and required.** PSA refuses datacenter addresses. |

#### Example inputs

Look up a collection:

```json
{ "mode": "cert", "certNumbers": ["49204884", "69300886", "147233572"] }
```

Pull the comps behind one card:

```json
{ "mode": "sales", "certNumber": "49204884" }
```

Sample a block of certificates graded around the same time:

```json
{ "mode": "range", "startCert": "49204884", "rangeCount": 50 }
```

### Output

#### `CERT` (cert and range modes)

```json
{
    "_input": "49204884",
    "recordType": "CERT",
    "certNumber": "49204884",
    "certUrl": "https://www.psacard.com/cert/49204884",
    "itemGrade": "GEM MT 10",
    "gradeNumber": 10,
    "year": "2018",
    "brand": "PANINI PRIZM EMERGENT",
    "subject": "LUKA DONCIC",
    "cardNumber": "3",
    "category": "BASKETBALL CARDS",
    "varietyPedigree": "EMERGENT",
    "labelType": "w/ Fugitive Ink Technology",
    "psaEstimate": 60.0,
    "psaEstimateRaw": "$60.00",
    "psaPopulation": 4923,
    "psaPopHigher": 0,
    "recentSalesCount": 5,
    "recentSaleMin": 46.0,
    "recentSaleMax": 70.0,
    "recentSaleAvg": 56.8,
    "latestSaleDate": "2026-09-15"
}
```

| Field | Type | Description |
|---|---|---|
| `itemGrade` / `gradeNumber` | string / number | Grade as printed, plus its numeric part for sorting. |
| `psaPopulation` | integer | How many copies exist at this grade. |
| `psaPopHigher` | integer | How many copies grade higher. `0` means this is a top-graded copy. |
| `psaEstimate` | number | PSA's own published value estimate at this grade. |
| `recentSaleMin` / `recentSaleMax` / `recentSaleAvg` | number | Range and plain average of the comps on the page. |
| `latestSaleDate` | string | Most recent comparable sale. |

#### `SALE` (sales mode)

```json
{
    "recordType": "SALE",
    "sourceCertNumber": "49204884",
    "saleIndex": 0,
    "subject": "LUKA DONCIC",
    "year": "2018",
    "soldPrice": 70.0,
    "soldDate": "2026-09-15",
    "seller": "eBay",
    "saleType": "FixedPrice",
    "grade": "PSA 10",
    "certNumber": "69300886",
    "lotNumber": "267742724000",
    "imageUrl": "https://i.ebayimg.com/images/g/aRgAAeSw6XlqarNX/s-l400.webp"
}
```

Each sale carries the certificate of the copy that sold, so you can look that one up in turn and build a chain of comparable cards.

#### `SURFACE` (reference mode)

```json
{
    "recordType": "SURFACE",
    "surface": "cert-page",
    "isAvailable": true,
    "detail": "payload parsed",
    "sample": { "certNumber": "49204884", "itemGrade": "GEM MT 10", "psaPopulation": "4,923", "recentSales": 5 }
}
```

#### Error rows

```json
{ "recordType": "ERROR", "_input": "999999999999", "_error": "not_found", "_errorDetail": "PSA has no such certification number" }
```

### What you can build with it

- **Collection valuation** - feed every certificate you own and get grade, scarcity and current market range in one sheet.
- **Rarity screens** - sort by `psaPopHigher = 0` to find top-pop copies, or by low `psaPopulation` for genuinely scarce grades.
- **Deal checking** - compare an asking price against `recentSaleMin`, `recentSaleAvg` and `psaEstimate` before you buy.
- **Market tracking** - schedule the same certificate list weekly and watch estimates and comps move.
- **Comp chains** - each sale row carries its own certificate number, so one card expands into its graded neighbours.

### Notes & limits

- **The full population report needs a PSA account.** This Actor returns the population figure the public certificate page prints, which is the population for that item at that grade, plus the count graded higher. It links to the full set report but does not open it.
- **Comparable sales are the ones PSA lists** - typically the five most recent. This is not a complete sales archive.
- **`psaEstimate` is PSA's own number**, not an appraisal by this Actor and not investment advice.
- **A residential proxy is required, not optional.** PSA answers home connections and refuses cloud ones; the Actor ships with Apify residential proxy enabled, and turning it off makes every row fail. This is also why the run costs a little proxy traffic.
- **The source is strict.** Keep `maxConcurrency` low; a wave of parallel lookups is the fastest way to get refused. The Actor negotiates the connection itself and records which profile worked in `_tlsProfile`.
- **Certificates are sequential**, which makes `range` mode useful for sampling a grading period - but most numbers in any block belong to cards you have no interest in.

### Scheduling tips

- Valuation refresh: weekly per certificate list is plenty; PSA's estimates do not move daily.
- New comps: `sales` mode weekly on the certificates you care about.
- Start any new schedule with one `reference` run to confirm the source is answering.

### Troubleshooting / FAQ

**A certificate returns `not_found`.** PSA has no record for that number. Check for a typo - the number is printed on the label under the barcode.

**`blocked_or_transient` on every row.** Almost always the proxy setting: PSA refuses datacenter IP addresses, so the residential proxy must stay enabled. If it is on, lower `maxConcurrency` and try again.

**`shape_changed`.** The page returned 200 but no item fields - PSA changed its layout. The row says so instead of shipping an empty record.

**Why is population empty for my cert?** A few item types (tickets, some memorabilia) do not publish one. The rest of the row is still complete.

**Can I search by player instead of certificate number?** Not in this Actor. The public entry point is the certificate number; searching by player needs the account-only sections.

### Legal & fair use

This Actor reads the public certificate verification page that PSA publishes for every graded item - the same page a buyer checks before a purchase - without an account and without touching the account-only population reports or auction-price archives. Prices and estimates are published market data, not investment advice.

# Actor input Schema

## `mode` (type: `string`):

cert = the graded item, its population, PSA's price estimate and a sales summary, one row per certification number. sales = every comparable sale PSA lists for that certificate, one row each. range = walk a block of consecutive certification numbers. reference = check the source is answering right now.

## `certNumber` (type: `string`):

A single PSA certification number, the number printed on the label. A full psacard.com/cert/... URL works too.

## `certNumbers` (type: `array`):

Several certification numbers to look up in one run.

## `startCert` (type: `string`):

First certification number of the block to walk. Certificates are issued sequentially, so a block covers cards graded around the same time.

## `rangeCount` (type: `integer`):

How many consecutive certification numbers to look up, starting at 'startCert'. Capped at 5000.

## `maxItems` (type: `integer`):

Maximum records per run.

## `maxConcurrency` (type: `integer`):

How many certificates to look up in parallel. Keep this modest - the source is behind a strict gate and hammering it invites a block.

## `proxyConfiguration` (type: `object`):

Residential proxy is REQUIRED and is the default. PSA refuses datacenter IP addresses outright: the same lookup that works from a home connection returns nothing from a cloud one. Leave this as it is unless you are supplying your own residential proxy.

## Actor input object example

```json
{
  "mode": "cert",
  "certNumber": "49204884",
  "certNumbers": [
    "49204884"
  ],
  "rangeCount": 25,
  "maxItems": 200,
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All certificate, comparable-sale and reference records produced by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "certNumber": "49204884",
    "certNumbers": [
        "49204884"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/psa-card-cert-lookup-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "certNumber": "49204884",
    "certNumbers": ["49204884"],
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/psa-card-cert-lookup-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "certNumber": "49204884",
  "certNumbers": [
    "49204884"
  ]
}' |
apify call fanndev/psa-card-cert-lookup-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/psa-card-cert-lookup-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/nLwirjPttM1duWIpo/builds/tAz5lrc1ig3HDMXoA/openapi.json
