Card Grading
To access the Ximilar API, first register at Ximilar App to get your API token. This API follows the general rules of Ximilar API as described in Section First steps.
The Card Grading service uses AI to automate the grading of trading and sports cards. For each image it locates the card, identifies its corners and edges, and computes grades for individual corners, edges, overall surface, and centering — similar to PSA, Beckett, CGC, and others.
It also identifies whether the image shows the Front or Back of the card, whether the card has an Autograph, and whether it is a TCG (trading card game) or a sports card (sport).
Because the API processes external images (e.g., user-generated content that may be low resolution, blurry, or compressed), it is best suited for soft grading or pre-grading. For the most accurate results, please provide high-resolution images (ideally with the shorter side at least 2000 px), unedited or unprocessed by smartphones, and without sleeves or slabs.
All access is asynchronous
Card grading is exposed only through the asynchronous request API. You submit a job, the backend queues it, a worker processes it, and you either poll for the result or receive a webhook callback. There are no synchronous card-grader/v2/* endpoints — the previous synchronous endpoints have been retired.
The full claim / poll / webhook lifecycle is documented at Async API. This page covers only the card-grader-specific payload shape and responses for each endpoint.
POST https://api.ximilar.com/account/v2/request/ # submit
GET https://api.ximilar.com/account/v2/request/__ID__ # poll status / read result
When submitting, set "type": "card-grader" and pick one of the five values for endpoint:
endpoint value | What it does |
|---|---|
grade | Full grading — centering, corners, edges, surface, final grade and condition label |
condition | Fast condition check only (Near Mint / Played / Damaged etc.), no per-region grades |
centering | Centering analysis only (left/right + top/bottom ratios + grade) |
localize | Card localization only — corners, edges, bounding box |
crop_level | Cropped + perspective-warped + auto-levelled card image uploaded to S3 |
Optional webhook field on the submit body lets you skip polling — the worker POSTs {id, status, response} to your URL after the job completes (see Async API for the webhook payload shape).
Submit a card-grading request
Submit a JSON body with type: "card-grader", the desired endpoint, your image records, and (optional) webhook.
The response gives you an id. The job state moves from CREATED → PROCESSING → DONE. Poll the GET endpoint or wait for the webhook.
Required attributes
- Name
Authorization- Type
- string
- Description
API token for authentication, sent as
Authorization: Token <your-token>.
- Name
type- Type
- string
- Description
Must be
"card-grader".
- Name
endpoint- Type
- string
- Description
One of
grade,condition,centering,localize,crop_level.
- Name
records- Type
- array
- Max
- Maximum:10
- Description
List of images to analyze. Each record must contain either
_urlor_base64. Forendpoint: "grade"the per-request limit is 2 (front + back of one card); for the other endpoints it is 10.
Optional attributes
- Name
webhook- Type
- object
- Description
{ "url": "...", "headers": { ... } }. When set, the worker POSTs the final{id, status, response}payload to this URL.
Submit request
curl --request POST \
--url https://api.ximilar.com/account/v2/request/ \
--header 'Authorization: Token __API_TOKEN__' \
--header 'content-type: application/json' \
--data '{
"type": "card-grader",
"endpoint": "grade",
"records": [{ "_url": "__IMAGE_URL__" }]
}'
Submit response
{
"id": "2e072580-fa20-47d7-aaa3-63ae32bcec7b",
"type": "card-grader",
"status": "CREATED",
"created": "2026-05-26T15:27:46.556452Z",
"workspace": "88431559-20ea-4d43-98a6-7a1cb38160e3",
"request": {
"type": "card-grader",
"endpoint": "grade",
"records": [{ "_url": "__IMAGE_URL__" }]
}
}
Poll for result
curl --request GET \
--url https://api.ximilar.com/account/v2/request/__ID__ \
--header 'Authorization: Token __API_TOKEN__'
GET response envelope when status="DONE"
{
"id": "61e14a37-037d-4581-9bf4-c9f001c86520",
"type": "card-grader",
"status": "DONE",
"created": "2026-05-27T12:51:53.649648Z",
"modified": "2026-05-27T12:51:58.456571Z",
"workspace": "88431559-20ea-4d43-98a6-7a1cb38160e3",
"request": { "type": "card-grader", "endpoint": "grade", "records": [...] },
"response": {
"type": "card-grader",
"endpoint": "grade",
"records": [ /* one entry per submitted record — shapes for each endpoint below */ ],
"statistics": { "time_stats": { "GradingProcessor": 8.78 } }
}
}
Per-endpoint sections below show only the inner record that lives at response.records[i]. The outer envelope is identical for all endpoints.
endpoint: "grade" — full grading
Returns:
- The positions of the card, its corners and edges.
- Individual grades for each (card, corners, edges).
- The results live in the
corners,edges, andcardfields. - An overall (averaged) grade named
finalinsidegrades, alongside the repeated per-component grades.
The _tags field on card[0] contains:
- the card
Category(e.g.,Trading Card GameorSport) - whether the card is
Damaged - whether it has an
Autograph - which
Sideis shown (FrontorBack)
All grades range from 1 to 10, where 1 is the worst and 10 is the best. The final grade is the geometric mean of corners, edges, surface, and centering:
final = geomean(geomean(CORNERS), geomean(EDGES), SURFACE, CENTERING)
If you submit both front and back in the same records array, the final grade is a weighted average: 70 % front and 30 % back by default. Adjust with the front_weight setting (range 0.0–1.0).
endpoint: "grade" accepts up to 2 records per request — typically the Front and Back of the same card.
Each image takes ~10–20 s to analyze. Sending both sides in one request counts as a single card for credit usage and enables weighted grading. Use the Side field in each image record to specify which is which.
Based on the final grade the response includes a condition label: Poor, Fair, Good, Very Good, Excellent, Near Mint, Mint, or Gem Mint.
Visualizations are uploaded to S3 and surfaced as:
_full_url_card— original image with detected objects and grades overlaid._exact_url_card— extracted upright card with centering visualization.
Returns (inside response.records[0])
- Name
_objects- Type
- array
- Description
Detected card object with
bound_boxand confidence.
- Name
_points- Type
- array
- Description
Four corner points in image coordinates (UL, UR, DR, DL).
- Name
corners- Type
- array
- Description
Per-corner
bound_box,point, andgrade.
- Name
edges- Type
- array
- Description
Per-edge
polygonandgrade.
- Name
card- Type
- array
- Description
Card-level info:
polygon,bound_box,surface.grade,centering,_tags.
- Name
grades- Type
- object
- Description
corners,edges,surface,centering,final,condition.
- Name
versions- Type
- object
- Description
Short hashes of every model used (
detection,points,corners,edges,surface,centering,final).
- Name
_full_url_card- Type
- string
- Description
- URL to the annotated overview image.
- Name
_exact_url_card- Type
- string
- Description
- URL to the extracted-card visualization.
Submit body
{
"type": "card-grader",
"endpoint": "grade",
"records": [
{ "_url": "__IMAGE_URL__" }
]
}
response.records[0]
{
"_id": "Submission IMAGE ID",
"_url": "__IMAGE_URL__",
"_status": {
"code": 200, "text": "OK",
"request_id": "5f6c8d38-f30f-4587-851e-3ee17f60fc1f"
},
"_width": 792,
"_height": 1068,
"_objects": [
{
"id": "e0f155e9-2978-4524-bae2-3d7d3377c2c4",
"name": "Card",
"prob": 0.93359375,
"bound_box": [0, 0, 792, 1068]
}
],
"_points": [
[17.0819, 7.3722], [767.3779, 7.8225],
[764.5919, 1057.4310], [18.8924, 1052.8937]
],
"corners": [
{ "name": "UPPER_LEFT", "bound_box": [0, 0, 69, 59], "point": [17, 7], "grade": 8.5 },
{ "name": "UPPER_RIGHT", "bound_box": [714, 0, 792, 59], "point": [767, 7], "grade": 7.5 },
{ "name": "DOWN_RIGHT", "bound_box": [711, 1004, 791, 1068], "point": [764, 1057], "grade": 8.5 },
{ "name": "DOWN_LEFT", "bound_box": [0, 999, 70, 1068], "point": [18, 1052], "grade": 8.5 }
],
"edges": [
{ "name": "UPPER", "polygon": [[72, -20], [711, -20], [711, 62], [72, 62]], "grade": 7.5 },
{ "name": "RIGHT", "polygon": [[711, 62], [794, 62], [791, 1001], [708, 1001]], "grade": 7.5 },
{ "name": "DOWN", "polygon": [[73, 996], [708, 1001], [708, 1084], [73, 1079]], "grade": 8.5 },
{ "name": "LEFT", "polygon": [[-10, 62], [72, 62], [73, 996], [-9, 996]], "grade": 8.0 }
],
"card": [
{
"name": "CARD",
"polygon": [[17, 7], [767, 7], [764, 1057], [18, 1052]],
"bound_box": [0, 0, 792, 1068],
"_tags": {
"Category": [{ "id": "44b6f95c-b3a5-4f5e-84d0-e6d441f19b3a", "name": "Card/Sport Card", "prob": 0.98710 }],
"Damaged": [{ "id": "8d77cbf2-96de-4e45-b518-ea09cf77483a", "name": "OK", "prob": 0.95114 }],
"Autograph": [{ "id": "45ef0f10-f4c5-4065-aaab-89384369b925", "name": "No", "prob": 0.66843 }],
"Side": [{ "id": "3ebc6fc4-41d4-4432-99f1-5f8c50f10413", "name": "Front", "prob": 0.94686 }]
},
"surface": { "grade": 6.0 },
"centering": {
"left/right": "46/54", "top/bottom": "54/46",
"bound_box": [32, 45, 711, 1010],
"pixels": [32, 45, 39, 39],
"offsets": [0.0437, 0.0434, 0.0522, 0.0377],
"grade": 9.0
}
}
],
"grades": {
"corners": 8.0, "edges": 8.0, "surface": 6.0,
"centering": 9.0, "final": 7.5, "condition": "Near Mint"
},
"versions": {
"detection": "segmentor",
"points": "segmentor",
"corners": "7516607f_1",
"edges": "b949c1b8_1",
"surface": "7ba660de_11",
"centering": "5bb1214a_4",
"final": "7516607f_1-b949c1b8_1-7ba660de_11-5bb1214a_4-segmentor-segmentor"
},
"_full_url_card": "https://s3-eu-west-1.amazonaws.com/ximilar-tmp-images/card-grading/1c54a360-5463-40ab-bf1d-5388e408e791.webp",
"_exact_url_card": "https://s3-eu-west-1.amazonaws.com/ximilar-tmp-images/card-grading/c026da43-81d1-4fb8-a75b-13f3b17c9d2d.webp"
}
Corner bound_box regions and edge polygon coordinates may extend slightly past the image bounds (e.g. negative values) when the segmentor detects a card flush with an image edge. Downstream cropping clamps these to the image — the documented coordinates are the raw output for reference.
endpoint: "condition" — fast condition check
Returns the position of the card (as a bounding box) and, for the largest detected card, a Condition label. You choose the naming mode:
ebay(default):- Conditions for TCG:
Near Mint,Lightly Played,Moderately Played,Damaged - Conditions for Sport:
Near Mint,Excellent,Very Good,Poor
- Conditions for TCG:
tcgplayer:- Conditions:
Near Mint,Lightly Played,Moderately Played,Heavily Played,Damaged
- Conditions:
ximilar:- Conditions:
Gem Mint,Mint,Near Mint,Excellent,Very Good,Good,Fair,Poor
- Conditions:
cardmarket:- Conditions:
Mint,Near Mint,Excellent,Good,Light Played,Played,Poor
- Conditions:
Maximum number of records in one request is 10.
Optional attributes (on the submit body root)
- Name
mode- Type
- string
- Default
- Default:ebay
- Description
Default is
ebay. Set totcgplayer,ximilar, orcardmarketto change the verbal grade table.
Submit body
{
"type": "card-grader",
"endpoint": "condition",
"mode": "ebay",
"records": [
{ "_id": "Submission IMAGE ID", "_url": "__IMAGE_URL__" }
]
}
response.records[0]
{
"_id": "Submission IMAGE ID",
"_url": "__IMAGE_URL__",
"_status": {
"code": 200, "text": "OK",
"request_id": "09859b9f-243f-4225-a031-1a5cf6b2fd14"
},
"_width": 792,
"_height": 1068,
"_objects": [
{
"id": "UNKNOWN",
"name": "Card",
"prob": 1.0,
"area": 1.0,
"bound_box": [0, 0, 792, 1068],
"Top Category": [
{ "id": "54c8ba01-545f-409d-beb9-d8dc2fecbb26", "name": "Card", "prob": 1.0 }
],
"Category": [
{ "id": "44b6f95c-b3a5-4f5e-84d0-e6d441f19b3a", "name": "Card/Sport Card", "prob": 0.98951 },
{ "id": "089fa0cd-a399-4c6c-8420-d52a55c0ff02", "name": "Card/Trading Card Game", "prob": 0.01049 }
],
"Condition": [
{
"id": "ad4f0eab-a513-4218-8a3c-dcc53a4223a6",
"name": "Card Condition Exact",
"value": 6.01532,
"label": "Excellent",
"mode": "ebay",
"scale": ["Poor", "Very Good", "Excellent", "Near Mint"],
"scale_value": 3,
"max_scale_value": 4
}
]
}
]
}
Notes on the _objects[0] payload
- Name
prob- Type
- number
- Description
Detection confidence for the card. The condition endpoint returns
1.0andbound_boxcovering the full image when no separate detection step is run (the Flow pipeline classifies the whole image directly).
- Name
Category- Type
- array
- Description
Sorted candidates with
nameandprob. The condition output is computed against the top entry (hereCard/Sport Card). Formode="ebay"this picks ebay-sport vs ebay-tcg automatically.
- Name
Condition- Type
- array
- Description
Single entry.
valueis the raw 1–10 grade;labelis the verbal grade in the chosenmode;scaleis the ordered list of labels for that mode;scale_valueandmax_scale_valuelet you render a position-on-scale UI without re-looking-up the table.
- Name
Top Category- Type
- array
- Description
Always
[{name: "Card", ...}]— present for compatibility with downstream tools that branch on broader taxonomy nodes.
endpoint: "centering" — centering analysis
Returns the position of the card and a centering analysis (left/right + top/bottom ratios with an overall grade). Visualizations are uploaded to S3 and surfaced as _clean_url_card and _exact_url_card. The _tags field provides the card Category, Autograph and Side.
Maximum number of records (batch) is 2 (front and back of the same card).
Optional per-record fields
- Name
transparent- Type
- boolean
- Description
If the image already has a card on a transparent (alpha) background, set to
trueto skip the heavy localization step.
- Name
skip-location- Type
- boolean
- Description
If the image is a clean scan with a perfectly aligned card, set to
trueto treat the whole image as the card.
- Name
no-image-output- Type
- boolean
- Description
If
true, the visualization images are not generated — faster.
Submit body
{
"type": "card-grader",
"endpoint": "centering",
"records": [
{ "_url": "__IMAGE_URL__" }
]
}
response.records[0]
{
"_id": "Submission IMAGE ID",
"_url": "__IMAGE_URL__",
"_status": {
"code": 200, "text": "OK",
"request_id": "d7ebc1ce-bde6-4f39-9b70-024085329809"
},
"_width": 792,
"_height": 1068,
"_objects": [
{
"id": "e0f155e9-2978-4524-bae2-3d7d3377c2c4",
"name": "Card",
"prob": 0.93359375,
"bound_box": [0, 0, 792, 1068]
}
],
"_points": [
[17.0819, 7.3722], [767.3779, 7.8225],
[764.5919, 1057.4310], [18.8924, 1052.8937]
],
"card": [
{
"name": "CARD",
"polygon": [[17, 7], [767, 7], [764, 1057], [18, 1052]],
"bound_box": [0, 0, 792, 1068],
"_tags": {
"Category": [{ "id": "44b6f95c-b3a5-4f5e-84d0-e6d441f19b3a", "name": "Card/Sport Card", "prob": 0.98710 }],
"Autograph": [{ "id": "45ef0f10-f4c5-4065-aaab-89384369b925", "name": "No", "prob": 0.66843 }],
"Side": [{ "id": "3ebc6fc4-41d4-4432-99f1-5f8c50f10413", "name": "Front", "prob": 0.94686 }]
},
"centering": {
"left/right": "46/54", "top/bottom": "54/46",
"bound_box": [32, 45, 711, 1010],
"pixels": [32, 45, 39, 39],
"offsets": [0.0437, 0.0434, 0.0522, 0.0377],
"grade": 9.0
}
}
],
"grades": { "centering": 9.0 },
"versions": { "detection": "segmentor", "points": "segmentor", "centering": "5bb1214a_4" },
"_clean_url_card": "https://s3-eu-west-1.amazonaws.com/ximilar-tmp-images/card-grading/2ada3b73-7717-4c2e-a408-5b7bc3eee2fa.webp",
"_exact_url_card": "https://s3-eu-west-1.amazonaws.com/ximilar-tmp-images/card-grading/fbb8bf8e-788e-4e60-aca3-150ca1f7835e.webp"
}
Notes on the centering payload
- Name
_points- Type
- array
- Description
Four corner points (UL, UR, DR, DL) returned by the segmentor in original-image coordinates as floats.
card[0].polygonis the integer-rounded version of the same points.
- Name
card[0].centering- Type
- object
- Description
left/rightandtop/bottomare human-readable percentage strings.pixelsis the per-side pixel margin[left, top, right, bottom].offsetsis each margin as a fraction of the card's relevant dimension.bound_boxis the centered inner-frame box in coordinates relative to the warped card crop.
- Name
_clean_url_card- Type
- string
- Description
S3 URL of the warped upright card (no overlay).
- Name
_exact_url_card- Type
- string
- Description
S3 URL of the warped card with centering visualization drawn on top.
- Name
versions- Type
- object
- Description
Short hashes of the models used (
detectionandpointsboth come from the segmentor service;centeringfrom the centering model).
endpoint: "localize" — card location only
Returns the positions of corners, edges, and the overall card in the image. No grades — fastest of the grading endpoints.
Optional per-record fields
- Name
transparent- Type
- boolean
- Description
If the image already has a card on a transparent (alpha) background, set to
trueto skip the heavy localization step.
Submit body
{
"type": "card-grader",
"endpoint": "localize",
"records": [
{ "_url": "__IMAGE_URL__" }
]
}
response.records[0]
{
"_id": "Submission IMAGE ID",
"_url": "__IMAGE_URL__",
"_status": {
"code": 200, "text": "OK",
"request_id": "b757ea33-7a78-4907-8d20-02b9f834affd"
},
"_width": 792,
"_height": 1068,
"_objects": [
{
"id": "e0f155e9-2978-4524-bae2-3d7d3377c2c4",
"name": "Card",
"prob": 0.93359375,
"bound_box": [0, 0, 792, 1068]
}
],
"_points": [
[17.0819, 7.3722], [767.3779, 7.8225],
[764.5919, 1057.4310], [18.8924, 1052.8937]
],
"corners": [
{ "name": "UPPER_LEFT", "bound_box": [0, 0, 69, 59], "point": [17, 7] },
{ "name": "UPPER_RIGHT", "bound_box": [714, 0, 792, 59], "point": [767, 7] },
{ "name": "DOWN_RIGHT", "bound_box": [711, 1004, 791, 1068], "point": [764, 1057] },
{ "name": "DOWN_LEFT", "bound_box": [0, 999, 70, 1068], "point": [18, 1052] }
],
"edges": [
{ "name": "UPPER", "polygon": [[72, -20], [711, -20], [711, 62], [72, 62]] },
{ "name": "RIGHT", "polygon": [[711, 62], [794, 62], [791, 1001], [708, 1001]] },
{ "name": "DOWN", "polygon": [[73, 996], [708, 1001], [708, 1084], [73, 1079]] },
{ "name": "LEFT", "polygon": [[-10, 62], [72, 62], [73, 996], [-9, 996]] }
],
"card": [
{
"name": "CARD",
"polygon": [[17, 7], [767, 7], [764, 1057], [18, 1052]],
"bound_box": [0, 0, 792, 1068]
}
],
"versions": { "detection": "segmentor", "points": "segmentor" }
}
The localize endpoint returns the same geometric fields as grade — _points, _objects, corners, edges, card — but no grade/surface/centering/grades values. Use it when you only need the card location (e.g., to build your own UI or pre-filter inputs before grading).
endpoint: "crop_level" — cropped and levelled card image
Given a card image, returns a URL to a new image that is cropped + perspective-warped + auto-levelled + sharpened. Useful as a normalised input for downstream tools or display.
Optional per-record fields
- Name
level- Type
- boolean
- Default
- Default:true
- Description
Defaults to
true. Set tofalseto skip the histogram-clip + sharpen step and upload the raw warped card. Useful when you want to apply your own color/levelling pipeline.
Submit body
{
"type": "card-grader",
"endpoint": "crop_level",
"records": [
{ "_url": "__IMAGE_URL__" },
{ "_url": "__IMAGE_URL_2__", "level": false }
]
}
response.records[0]
{
"_id": "Submission IMAGE ID",
"_url": "__IMAGE_URL__",
"_status": {
"code": 200, "text": "OK",
"request_id": "2195f836-df4f-4735-8406-b282e6481dc0"
},
"_width": 792,
"_height": 1068,
"_img_url": "https://s3-eu-west-1.amazonaws.com/ximilar-tmp-images/card_crop_level/9bfc46c3-701d-4282-8bd5-6df22701b345.webp",
"versions": { "detection": "segmentor", "points": "segmentor" }
}
- Name
_img_url- Type
- string
- Description
S3 URL of the cropped, perspective-warped (and, by default, auto-levelled + sharpened) card. The image is uploaded as
.webpto theximilar-tmp-images/card_crop_level/prefix; URLs are publicly readable.
- Name
versions- Type
- object
- Description
Short hashes / source labels of the localization step. For the crop endpoint both
detectionandpointscome from the segmentor service.