Loading...
Loading...
Sign in and generate a token from Settings → API Keys
POST front/back images to /api/v1/grade
Poll GET /api/v1/grade/:id until status is "completed"
# Step 1: Submit a card for grading curl -X POST https://cardgrade.io/api/v1/grade \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -F "frontImage=@card_front.jpg" \ -F "backImage=@card_back.jpg" \ -F "gradingCompany=CGI" \ -F "cardType=Pokemon Card" # Step 2: Check grading status curl https://cardgrade.io/api/v1/grade/GRADING_ID \ -H "Authorization: Bearer YOUR_API_TOKEN"
Include your API token in the Authorization header of every request.
Authorization: Bearer YOUR_API_TOKENGenerate tokens in your dashboard. Tokens expire after 1 year. You can have up to 10 active tokens.
Keys are scoped. A Read-only key can read gradings, reports, and account info but cannot submit new gradings or spend credits; a Full access key can do both. Pick the scope when you create the key.
/api/v1/gradeSubmit a card for AI grading
| Name | Type | Required | Description |
|---|---|---|---|
| frontImage | File | Yes | Front image of the card (JPEG, PNG, or WebP, max 10MB per image). IMAGE QUALITY DRIVES ACCURACY: the grader resolves fine corner/edge defects (whitening, fraying) from image detail, so send the highest-resolution, sharpest, glare-free photo you can — ~3000–4000px on the long edge is ideal. Low-resolution or heavily-compressed images measurably reduce fine-defect accuracy. Do NOT downscale before uploading; full resolution is preserved server-side. |
| backImage | File | No | Back image of the card (JPEG, PNG, or WebP, max 10MB per image, counted separately from the front — up to 20MB per submission) |
| gradingCompany | string | No | Free-text label printed on the certificate (e.g. CGI, PSA, BGS, CGC, SGC; default "CGI"). Does not change the grading scale — every card is scored on the CardGrade unified rubric, and each completed result returns psaPrediction, bgsPrediction and cgcPrediction for cross-company estimates. |
| cardType | string | No | Card type (default: "Pokemon Card") |
| Idempotency-Key | string (header) | No | Optional unique key (e.g. a UUID). Safely retry a failed/timed-out request with the same key — you get the original grading back instead of being charged a second credit. |
{
"id": "abc123",
"status": "pending",
"creditsRemaining": 79
}/api/v1/grade/validateDry-run a submission. Runs the exact same auth, scope, and image validation as POST /grade but spends NO credit, creates no grading, and reports EVERY problem at once (wrong field name, content-type, oversized image) instead of stopping at the first. Use it while building your integration.
| Name | Type | Required | Description |
|---|---|---|---|
| frontImage | File | Yes | Same fields as POST /grade. Checked for presence, content-type (JPEG/PNG/WebP), and size (<10MB) — no image processing or grading is performed. |
| backImage | File | No | Optional, same rules as POST /grade. |
{
"valid": true,
"wouldSpendCredits": 1,
"creditsRemaining": 79,
"sufficientCredits": true,
"request": {
"frontImage": { "contentType": "image/jpeg", "sizeBytes": 482133 },
"backImage": null
},
"note": "Dry run only — no credit was spent and no grading was created."
}/api/v1/grade/:idCheck grading status and retrieve results. For a rejected card, status is "failed" and a "rejection" object explains why (the credit is always refunded).
| Name | Type | Required | Description |
|---|---|---|---|
| id | string (path) | Yes | Grading ID returned from POST /grade |
{
"id": "abc123",
"status": "completed",
// If status is "failed", there is no "results" block. Instead you get a
// "rejection" object with the same honest reason app users see — so you can
// tell the customer how to fix the photo instead of showing a bare "failed".
// The credit is ALWAYS refunded on a failed grade.
// "status": "failed",
// "rejection": {
// "code": "card_cropped", // machine-readable reason (or null)
// "message": "We had trouble reading your card's edges — this can happen
// with shiny holo/foil cards. Your credit has been refunded.
// Try again with the bare card flat on a dark, matte surface."
// }
"cardName": "Charizard VMAX",
"overallGrade": 9.2,
"results": {
"centering": 9.5,
"corners": 9.0,
"edges": 9.0,
"surface": 9.5,
"psaPrediction": 9,
"bgsPrediction": 9.0,
"cgcPrediction": 9.0,
// Per-zone VISUAL defect classification (none | minor | moderate | severe)
// for every corner and edge. This is the breakdown behind each sub-grade —
// inspect it to see exactly what drove a score (e.g. whitening at one corner).
"frontLlmObservations": {
"corners": {
"topLeft": { "whitening": "none", "rounding": "none", "fraying": "none", "cornerTouches": "none", "damage": "none" },
"topRight": { "whitening": "minor", "rounding": "none", "fraying": "none", "cornerTouches": "none", "damage": "none" },
"bottomLeft": { "...": "..." },
"bottomRight":{ "...": "..." }
},
"edges": { "top": { "...": "..." }, "left": { "...": "..." }, "right": { "...": "..." }, "bottom": { "...": "..." } }
},
"backLlmObservations": { "...": "..." },
// Per-zone GEOMETRIC tolerance, 0–1000 (1000 = perfect). Orthogonal to the
// visual observations above — a corner can be geometrically perfect yet show
// visual whitening, or vice-versa.
"cvMetrics": { "corners": { "...": "..." }, "centering": { "...": "..." } },
// Close-up crops the grader actually analyzed (per corner/edge + full card).
"zoneCropUrls": { "front-topLeft": "https://cardgrade.io/api/images/.../front-topLeft.jpg", "...": "..." }
},
"certificateUrl": "https://cardgrade.io/c/AbCdEfGh1234",
"reportImageUrl": "https://cdn.cardgrade.io/gradings/<userId>/abc123/report.jpg",
"shareToken": "AbCdEfGh1234"
}/api/v1/grade/:id/reportGet the eBay-ready grading report image (1200×1800 JPG) — perfect for PictureURL slots on a listing. Auto-generates the certificate and report on first call; subsequent calls 302-redirect to the cached CDN URL.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string (path) | Yes | Grading ID returned from POST /grade. Must be a completed grading. |
# Cached path (most calls): HTTP/1.1 302 Found Location: https://cdn.cardgrade.io/gradings/<userId>/<gradingId>/report.jpg # First call (renders synchronously, ~1–2s): HTTP/1.1 200 OK Content-Type: image/jpeg Cache-Control: public, max-age=3600, must-revalidate X-Report-Image-Url: https://cdn.cardgrade.io/gradings/.../report.jpg <binary JPG bytes>
/api/v1/gradesList your grading history with pagination
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer (query) | No | Results per page (1-100, default: 20) |
| offset | integer (query) | No | Pagination offset (default: 0) |
| status | string (query) | No | Filter by status: pending, processing, completed, failed |
{
"gradings": [
{ "id": "abc123", "status": "completed", "cardName": "Charizard", "overallGrade": 9.2, ... },
{ "id": "def456", "status": "pending", ... }
],
"limit": 20,
"offset": 0
}/api/v1/accountGet your account info, credits, and rate limits
{
"id": "user_123",
"plan": "pro",
"credits": 72,
"packCredits": 20,
"totalCredits": 92,
"rateLimits": {
"gradesPerMinute": 10,
"readsPerMinute": 50
}
}Grading is asynchronous. Instead of polling GET /api/v1/grade/:id, register a webhook and we'll POST the finished result to your URL the moment it's ready. Add one in your dashboard under Webhooks — included on every plan with API access (up to 10 endpoints).
grading.completedWe send a POST with these headers and body. The data object is identical to the GET /api/v1/grade/:id response. Reply with any 2xx within 10 seconds.
POST https://your-server.com/your-webhook
Content-Type: application/json
X-CardGrade-Event: grading.completed
X-CardGrade-Signature: sha256=<hmac>
{
"event": "grading.completed",
"data": { "id": "abc123", "status": "completed", "overallGrade": 9.2, "...": "..." },
"timestamp": "2026-06-16T14:05:00.000Z"
}Each webhook has its own signing secret (shown once when you create it). Compute an HMAC-SHA256 of the raw request body with that secret and compare it to the X-CardGrade-Signature header. Reject anything that doesn't match.
import crypto from 'crypto'
const sig = req.headers['x-cardgrade-signature'] // "sha256=abc..."
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody) // raw bytes, not parsed JSON
.digest('hex')
if (sig !== expected) return res.status(401).end() // not from CardGradeRate limits are applied per user and vary by plan tier.
10 grades/min
50 read requests/min
30 grades/min
150 read requests/min
30 grades/min
150 read requests/min
60 grades/min
300 read requests/min
120 grades/min
600 read requests/min
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests per minute |
| X-RateLimit-Remaining | Requests remaining in the current window |
| X-Request-Id | Unique request ID for debugging |
Dedicated Developer API plans, billed monthly — intro pricing is available. API access is also included on Pro and Business. Full comparison on the API product page.
New integrations and smaller apps.
Intro pricing available
Scaling marketplaces and trend apps.
Intro pricing available
High-volume and bulk grading.
Intro pricing available
Monthly billing starts at checkout — cancel anytime. Need more than 12,000 grades/mo or custom terms? Talk to us.
All error responses include an error message and a machine-readable code.
| Code | HTTP Status | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid API token |
| FORBIDDEN | 403 | Account suspended or email not verified |
| PLAN_REQUIRED | 403 | API access requires an active Pro, Business, or Developer API plan |
| INSUFFICIENT_SCOPE | 403 | This key lacks the scope for this operation (e.g. a read-only key calling POST /grade) |
| RATE_LIMITED | 429 | Too many requests — check X-RateLimit headers |
| INSUFFICIENT_CREDITS | 402 | No credits remaining — purchase a credit pack or upgrade |
| INVALID_INPUT | 400 | Missing or invalid request parameters |
| NOT_FOUND | 404 | Resource not found or not owned by you |
| INTERNAL_ERROR | 500 | Server error — retry or contact support |
# Example error response
{
"error": "Rate limit exceeded. Please try again later.",
"code": "RATE_LIMITED"
}