# Confirm centering lines

> Send lines reviewed by a person for client-confirmed centering-only grade ceilings.

- **Endpoint**: `POST /v1/centering/confirm`
- **Cost**: 5 credits; verified Free trial: 5 measure and 5 confirm calls, independently. Developer excluded.
- **Minimum plan**: growth
- **Cache-Control**: `private, no-store`

Source: https://pokemontcgapi.com/docs/api/centering/confirm

No key yet? One call gets you one: `POST /v1/accounts/free` with an `Idempotency-Key` header and your email address returns a working key in the response. Keep the original Idempotency-Key and body for response replay for up to 24 hours, under the conditions in the quickstart. The trial is 800 credits over 30 days once the address is confirmed, no card; paid plans start at 29 € a month. See the [quickstart](https://pokemontcgapi.com/docs/quickstart) and the [pricing](https://pokemontcgapi.com/pricing).

A person must inspect or move the cut and print lines in your app before this call. Send the geometry from the preceding measure, keeping the measured face and size. The result has `basis: client-confirmed-lines` and centering-only ceilings for PSA/BGS/CGC/SGC. The response example is an excerpt showing only PSA; the actual response includes all four houses. Confirm does not return geometry; keep the reviewed geometry in the app.

Growth and higher; Developer is excluded. A verified Free trial includes 5 measure and 5 confirm calls, with independent counters. Grade and standards are available during the verified trial without separate call counters. All calls still consume credits. Requires `vision:read`. See the [two-step centering guide](https://pokemontcgapi.com/docs/centering).

Always display: ceiling from centering alone, not a prediction of the final grade. Results are client-confirmed when a person checks the lines; never describe them as verified or certified.

Validation uses `400 INVALID_PARAMETER`: service validation includes `details.service_code` and `details.param`; upload validation may carry only `details.param`. Busy, failed or timed-out processing returns `503 PROVIDER_UNAVAILABLE` with `Retry-After: 2`. A deployment without centering returns `503 FEATURE_NOT_CONFIGURED`. Ineligible plans, unverified Free accounts or exhausted measure/confirm trial counters return `403 PLAN_REQUIRED`; follow `details.next_step` when present. Errors refund credits and trial reservations.

## Body fields

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `face` **required** | enum | — | front or back; preserve the measured face. |
| `card_size` **required** | enum | — | standard or small; must match geometry.card_size. |
| `geometry` **required** | object | — | Geometry v1 from measure, after a person has reviewed or adjusted its cut and print lines. Preserve dimensions, corners, rotations, corner_offset, warnings and positioning_sigma. |
| `confirmed_by` **required** | enum | — | Must be person. This is a client assertion that a person checked the lines, never an assertion an assistant can make. |

## Example request

**curl**

```bash
# Run only after a person has checked/adjusted the lines in your app.
jq '{face:.data.face,card_size:.data.card_size,geometry:.data.geometry,confirmed_by:"person"}' reviewed-measure.json > confirm.json
curl -s "https://api.pokemontcgapi.com/v1/centering/confirm" -H "X-Api-Key: $PTCG_API_KEY" -H "Content-Type: application/json" --data-binary @confirm.json
```

**JavaScript**

```js
// measurement is the preceding measure response; reviewedGeometry comes from your line editor.
// This handler runs only after the person explicitly accepts the displayed lines.
async function onPersonAccepted(reviewedGeometry) {
  const res = await fetch("https://api.pokemontcgapi.com/v1/centering/confirm", {
    method: "POST",
    headers: { "X-Api-Key": process.env.PTCG_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ face: measurement.data.face, card_size: measurement.data.card_size,
      geometry: reviewedGeometry, confirmed_by: "person" }),
  });
  if (!res.ok) throw new Error(JSON.stringify(await res.json()));
  const result = await res.json();
  console.log(result.data.ceilings, result.data.basis, result.data.disclaimer);
}
```

## Example response

`200 OK` · `Cache-Control: private, no-store`

`response.json`

```json
{
  "data": {
    "status": "ok",
    "code": "OK",
    "face": "front",
    "card_size": "standard",
    "lr": {
      "ratio": "57/43",
      "major_percent": 57.043033695585834,
      "minor_percent": 42.956966304414166,
      "uncertainty_pp": 3.1,
      "sigma_pp": 1.4308512800184152,
      "worst_station_px": 350,
      "stations_major_percent": [
        57.043033695585834,
        56.993401021753,
        56.94400625495934
      ]
    },
    "tb": {
      "ratio": "52/48",
      "major_percent": 51.98633109838324,
      "minor_percent": 48.01366890161676,
      "uncertainty_pp": 2.9,
      "sigma_pp": 1.3316090517824892,
      "worst_station_px": 250,
      "stations_major_percent": [
        51.98633109838324,
        51.97251051504912,
        51.95869275717155
      ]
    },
    "confidence": 0.9499202051993555,
    "warnings": [],
    "ceilings": {
      "PSA": {
        "maximum_grade": 9,
        "label": "MINT",
        "rank": 90,
        "limiting_face": "front",
        "limiting_axis": "lr",
        "distance_to_next": 2.0430336955858337,
        "next_grade": 10,
        "next_label": "GEM MT",
        "borderline": true,
        "unconstrained": false,
        "source_status": "official",
        "standard_status": "official",
        "source": "https://www.psacard.com/gradingstandards",
        "sources": [
          "https://www.psacard.com/gradingstandards"
        ],
        "reviewed_at": "2026-10-01",
        "verification": "retrieved"
      }
    },
    "advice": "Lines confirmed by the client. Measure the other face too; a person checked the lines in your application.",
    "elapsed_ms": 0,
    "normalizations": [],
    "image_bytes": 0,
    "summary": "front: L/R 57/43 ±3.1 pp; T/B 52/48 ±2.9 pp. PSA: 9 MINT; BGS: 8.5 NM-Mint+ (interpreted); CGC: 9 Mint (interpreted); SGC: 9 Mint (approximate). Lines confirmed by the client. Measure the other face too; a person checked the lines in your application. This is a ceiling from centering alone, not a prediction of the final grade.",
    "disclaimer": "This is a ceiling from centering alone, not a prediction of the final grade.",
    "basis": "client-confirmed-lines"
  },
  "meta": {
    "elapsed_ms": 0,
    "engine_version": "1.0.0"
  }
}
```

## Errors

Every error body carries `error.code`, `error.message` and `error.request_id`. Switch on the code, never on the message.

| Status | Code | When |
| --- | --- | --- |
| 401 | `MISSING_API_KEY` | No `X-Api-Key` header and no bearer token on a route that requires one. |
| INSUFFICIENT_SCOPE | INSUFFICIENT_SCOPE | — |
| 403 | `PLAN_REQUIRED` | The feature is outside the plan (movers require Growth; photo recognition has 5 calls after Free email confirmation, then requires Growth), or the account subscription is canceled or suspended. Centering requires Growth or higher; Developer is excluded. After email confirmation the trial includes 5 measure and 5 confirm calls independently; grade and standards use trial credits without separate call counters. Before email confirmation, the message starts with “Confirm your email…”. Follow details.next_step when present. |
| 400 | `INVALID_PARAMETER` | A query parameter has the wrong type or an unsupported value — a non-integer `limit`, an unknown `lang`, an unknown `region`, a sort key that is not sortable. |
| PROVIDER_UNAVAILABLE | PROVIDER_UNAVAILABLE | — |
| 503 | `FEATURE_NOT_CONFIGURED` | The endpoint exists but its optional backend is not configured in this deployment. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | The `Content-Type` is not one this route accepts. |
| 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than the server accepts. |
| 429 | `QUOTA_EXCEEDED` | Trial credits, monthly credits or the daily cap are exhausted, and `details` says which: `daily_cap` with `retry_after_s` is the trial daily cap, which resets at midnight UTC; `renews: true` with `resets_at` is the monthly quota of a paid plan; `renews: false` is the trial allowance, which never comes back. `details.next_step` carries the plan, prices and owner action, or email verification when it unlocks the full trial. |
| 403 | `TRIAL_EXPIRED` | The trial has ended: the account is on the trial and its 30 days from signup are over, and this route costs credits. `details.trial_expired_at` is the date, `details.trial_days` the length, and `details.next_step` the owner handoff. Zero-cost routes — `/v1/me`, `/v1/me/usage`, the key routes and `/v1/billing/checkout` — still answer. |

The full taxonomy, with what to do about each code, is on the [errors](https://pokemontcgapi.com/docs/errors) page.
