Card centering from a photo: measure first, confirm the lines second
Build a two-step card centering flow: photo to ratios and geometry, person-reviewed lines to centering-only ceilings, with TypeScript, Python and Go.
Measure a card's centering from a photo. The API returns the L/R and T/B ratios with their uncertainty and the detected corners and border lines; a person reviews the lines in your app and confirms them, and the API returns the centering-only grade ceilings for PSA, BGS, CGC and SGC. Growth and higher; a verified trial includes 5 measure and 5 confirm calls. Measure 25 credits, confirm 5, grade 1, standards 1.
This is a ceiling from centering alone, not a prediction of the final grade.
Why there are two steps
A phone photo contains a card, a background, shadows and sometimes a sleeve. The outer cut and the printed border are different lines, and an automatic detection can place either one on a highlight or a dark edge. A ratio can look plausible even when the line that produced it is wrong. Returning a grade without someone looking at those lines would turn a detection into a promise it cannot support.
On real photos, measure almost always answers HTTP 200 with status: needs-review and ceilings: null. That is a result to review, not a failed HTTP request. It supplies the L/R and T/B ratios, their uncertainty and, when detection permits, geometry for an editor. The application shows the evidence before asking the person to accept it. Only then does confirm return the photo-based ceilings. This is a ceiling from centering alone, not a prediction of the final grade.
Keep the distinction between an HTTP error, status: failed and needs-review. A failed detection needs a better photo or corrected corners. If geometry is absent, offer a retake rather than a confirm button. If it is present, uncertainty and warnings belong beside the overlay, where the person can act on them.
Photo, geometry, review, confirm
- Send a photo of one face to POST /v1/centering/measure, with face: front or back and the actual card_size. For a borderless front, use the back.
- Show the ratios and uncertainty and draw the detected corners and cut and print lines. If the corners are wrong, collect corrected corners and call measure again.
- Let a person inspect or adjust the cut and print lines. Preserve the full geometry from measure, including dimensions, rotations, corner_offset, warnings and positioning_sigma.
- Only after that person explicitly accepts the lines, send POST /v1/centering/confirm with the reviewed geometry, the same face and card_size, and confirmed_by: person.

sv1a 080/073), photographed with a phone on dark cloth and cropped around the card before upload. Measure returned L/R 50/50 ±15.1 pp and T/B 51/49 ±17.3 pp. After a person reviewed the lines, confirm returned L/R 50/50 ±1.7 pp, T/B 51/49 ±1.7 pp and ceilings of PSA 10 (official), BGS 10 (interpreted), CGC 10 (interpreted) and SGC 10 (approximate). This is a ceiling from centering alone, not a prediction of the final grade.
status: needs-review, code: HIGH_UNCERTAINTY, L/R 52/48 ±23.4 pp and T/B 52/48 ±25.7 pp. After a person reviewed the lines, confirm returned L/R 52/48 ±1.6 pp, T/B 52/48 ±1.6 pp and ceilings of PSA 10, BGS 10 and CGC 10; SGC was null because it has no published back table. Grade with both faces returned 10 for PSA, BGS, CGC and SGC. This is a ceiling from centering alone, not a prediction of the final grade.The geometry guide matters when you build the overlay: corners use EXIF-oriented image pixels after reduction, while lines use the rectified coordinate system including padding. A projective transform connects them; drawing rectified line coordinates straight onto the original camera image will put the review in the wrong place. The line editor guide provides the formulas and a tested JavaScript implementation.
Confirm returns ratios and ceilings with basis: client-confirmed-lines. That basis records your application’s assertion that a person reviewed the lines. It does not return another geometry, so keep the geometry the person accepted in your app. This is a ceiling from centering alone, not a prediction of the final grade.
The same flow in three SDKs
These examples deliberately stop at the editor boundary. The callback belongs to the explicit acceptance action in your interface; a script calling it immediately after measure would skip the review the API requires. Use a server-side key and keep it out of browser code.
import { readFile } from "node:fs/promises";
import { PokemonTcgApi, type CenteringGeometry } from "@pokemontcgapi/sdk";
const client = new PokemonTcgApi({ apiKey: process.env.PTCG_API_KEY });
const measurement = await client.centering.measure(await readFile("card.jpg"), {
face: "front", card_size: "standard",
});
const measured = measurement.data;
if (!measured.geometry) throw new Error("Retake the photo before line review");
console.log(measured.status, measured.lr, measured.tb, measured.warnings);
// Render measured.geometry and let a person inspect or adjust its lines.
// Connect only to the person's explicit acceptance in your line editor.
async function onPersonAccepted(reviewedGeometry: CenteringGeometry) {
const { data } = await client.centering.confirm({
face: measured.face, card_size: measured.card_size,
geometry: reviewedGeometry, confirmed_by: "person",
});
console.log(data.ceilings, data.basis);
console.log("This is a ceiling from centering alone, not a prediction of the final grade.");
}from pokemontcgapi import PokemonTcgApi
from pokemontcgapi.types import CenteringGeometry
client = PokemonTcgApi() # reads PTCG_API_KEY
measurement = client.centering.measure("card.jpg", face="front", card_size="standard")
measured = measurement["data"]
if not measured.get("geometry"):
raise ValueError("Retake the photo before line review")
print(measured["status"], measured["lr"], measured["tb"], measured["warnings"])
# Render measured["geometry"] and let a person inspect or adjust its lines.
# Connect only to the person's explicit acceptance in your line editor.
def on_person_accepted(reviewed_geometry: CenteringGeometry):
result = client.centering.confirm({
"face": measured["face"], "card_size": measured["card_size"],
"geometry": reviewed_geometry, "confirmed_by": "person",
})
print(result["data"]["ceilings"], result["data"]["basis"])
print("This is a ceiling from centering alone, not a prediction of the final grade.")package main
import (
"context"
"fmt"
"os"
"github.com/pokemontcgapi/sdk-go"
)
// Your app calls this after identification and keeps the returned callback
// for the person's explicit acceptance in its line editor.
func startReview(ctx context.Context, c *pokemontcgapi.Client) (func(context.Context, pokemontcgapi.CenteringGeometry) error, error) {
photo, err := os.Open("card.jpg")
if err != nil { return nil, err }
defer photo.Close()
measurement, err := c.Centering.Measure(ctx, photo, pokemontcgapi.CenteringMeasureOptions{
Face: "front", CardSize: "standard",
})
if err != nil { return nil, err }
measured := measurement.Data
if measured.Geometry == nil { return nil, fmt.Errorf("retake the photo before line review") }
fmt.Println(measured.Status, measured.LR, measured.TB, measured.Warnings)
// Render measured.Geometry; do not call onPersonAccepted automatically.
onPersonAccepted := func(ctx context.Context, reviewedGeometry pokemontcgapi.CenteringGeometry) error {
result, err := c.Centering.Confirm(ctx, pokemontcgapi.CenteringConfirmInput{
Face: measured.Face, CardSize: measured.CardSize,
Geometry: reviewedGeometry, ConfirmedBy: "person",
})
if err != nil { return err }
fmt.Println(result.Data.Ceilings, result.Data.Basis)
fmt.Println("This is a ceiling from centering alone, not a prediction of the final grade.")
return nil
}
return onPersonAccepted, nil
}Python also exposes the same four methods through AsyncPokemonTcgApi: await client.centering.measure, confirm, grade and standards. The SDK method table lists the signatures for all three clients. The REST equivalents and error shapes are in the centering reference.
What a ceiling says
Confirm returns ceilings for PSA, BGS, CGC and SGC after the person’s review. Show each house’s maximum_grade together with source_status and borderline. The source status can be official, interpreted or approximate: preserve that distinction rather than turning every threshold into a company promise. The source dates describe when thresholds were reviewed, not a review of the card in the photo. This is a ceiling from centering alone, not a prediction of the final grade.
The other face or a missing axis can lower the ceiling. For known ratios, grade reports incomplete, missing_faces and missing_axes; do not fill gaps by guessing. SGC has no published back table, and an unconstrained result can have maximum_grade: null. A null is something to explain, not a reason to invent a number.
Use GET /v1/centering/standards for threshold bands, source URLs and rounding rules. Use POST /v1/centering/grade when the ratios are already known. Grade is a calculation from supplied ratios; it is not a substitute for the person reviewing the lines of a photo before confirm. This is a ceiling from centering alone, not a prediction of the final grade.
Costs, plans and the trial
| Route | Credits |
|---|---|
| POST /v1/centering/measure | 25 |
| POST /v1/centering/confirm | 5 |
| POST /v1/centering/grade | 1 |
| GET /v1/centering/standards | 1 |
Ongoing access is included on Growth, Professional, Enterprise. Developer is excluded with 403 PLAN_REQUIRED. Every centering route requires vision:read, which also covers photo recognition. After email confirmation, the Free trial includes 5 measure and 5 confirm calls independently of each other and Vision. Grade and standards spend trial credits without separate call counters. These calls still use the trial’s 800 credits; the trial lasts 30 days from signup.
Before email confirmation, PLAN_REQUIRED asks the account owner to confirm the email. Read details.next_step when present, and stop on a plan refusal rather than retrying. Busy processing returns 503 PROVIDER_UNAVAILABLE with Retry-After: 2; credits and trial reservations are refunded. Back off with jitter instead of sending a burst of retries.
An assistant can measure, but cannot attest to your review
MCP exposes ptcg_measure_centering, ptcg_grade_centering and ptcg_get_centering_standards. There is deliberately no confirm tool: an assistant cannot assert that a person inspected the lines. Build that action into the application that shows the photo and collects the person’s acceptance, then call confirm there for the photo-based ceilings. This is a ceiling from centering alone, not a prediction of the final grade.
Build the editor before the grade display
Start with the two-step guide, then wire its geometry into your review interface. Keep ratios, uncertainty, source status and the limitation visible wherever you display a ceiling. This is a ceiling from centering alone, not a prediction of the final grade.