{
  "openapi": "3.1.0",
  "info": {
    "title": "CardGrade API",
    "version": "1.0.0",
    "summary": "AI trading-card pre-grading as a REST API.",
    "description": "Submit card images and receive an image-based condition pre-screen: overall score, centering, corner, edge and surface scores, per-zone details, AI analysis, market-value fields when available, and a shareable report image.\n\nAuthentication uses a Bearer API key (prefixed `cgrd_`) generated in your dashboard under Settings -> API. API access requires an eligible Pro, Business, or dedicated Developer API plan with an active or trialing status.\n\nResponses are bare JSON objects (there is no `{success, data}` envelope). Errors are bare `{error, code}` objects with the matching HTTP status. Every response carries `X-Request-Id` and, where applicable, `X-RateLimit-Limit` / `X-RateLimit-Remaining` headers; `429` responses include `Retry-After`.\n\nKeys are scoped. A Read-only key carries `grades:read` + `account:read` and cannot submit gradings (no credit spend). A Full-access key adds `grades:write`. Legacy keys created before scopes existed are unrestricted.",
    "contact": {
      "name": "CardGrade Support",
      "email": "support@cardgrade.io",
      "url": "https://cardgrade.io/api-docs"
    }
  },
  "servers": [
    {
      "url": "https://cardgrade.io",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Grading",
      "description": "Submit and retrieve card gradings."
    },
    {
      "name": "Account",
      "description": "Account, plan, and credit information."
    }
  ],
  "paths": {
    "/api/v1/grade": {
      "post": {
        "tags": [
          "Grading"
        ],
        "operationId": "submitGrade",
        "summary": "Submit a card for grading",
        "description": "Uploads a card image and queues an AI grade. Deducts one credit. Required scope: `grades:write`.\n\nGenerate one UUID per logical submission and send it in the `Idempotency-Key` header. Keys are scoped to the authenticated account and have no time-based TTL; they remain replayable for the lifetime of the grading record. Reusing a key returns the first grading even if later images or parameters differ, so never recycle a key across submissions. Once the key has a grading record, a POST replay may omit the multipart body and images; it returns the same grading ID, its current status, the account's current credit balance, and `idempotent: true` without charging another credit. For low-bandwidth recovery, prefer `GET /api/v1/grade/by-idempotency-key` with the same header and no body or images. A `404 IDEMPOTENCY_KEY_NOT_FOUND` from that lookup means no grading record exists for the key in the authenticated account, so resend the original multipart POST with the same key and images.\n\nThe grade runs asynchronously \u2014 retrieve the full result from `GET /api/v1/grade/{id}` when `status` is `completed`.",
        "requestBody": {
          "description": "Required for a new submission. May be omitted only when replaying an Idempotency-Key that already has a grading record.",
          "required": false,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "frontImage"
                ],
                "properties": {
                  "frontImage": {
                    "type": "string",
                    "format": "binary",
                    "description": "Front of the card. JPEG, PNG, or WebP. Max 10 MB."
                  },
                  "backImage": {
                    "type": "string",
                    "format": "binary",
                    "description": "Back of the card (optional but recommended for full grading). Same constraints."
                  },
                  "gradingCompany": {
                    "type": "string",
                    "default": "CGI",
                    "description": "Grading standard to grade against."
                  },
                  "cardType": {
                    "type": "string",
                    "default": "Pokemon Card",
                    "description": "Card category hint for identification."
                  },
                  "userNotes": {
                    "type": "string",
                    "description": "Optional free-text note (not stored on the grade)."
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "201": {
            "description": "Grade accepted and queued.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeAccepted"
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay \u2014 returns the first grade recorded for this account and Idempotency-Key, including its current status and the account's current credit balance. No credit was charged for the replay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentGradeReference"
                },
                "example": {
                  "id": "d71bb389-...",
                  "status": "completed",
                  "creditsRemaining": 79,
                  "idempotent": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/grade/by-idempotency-key": {
      "get": {
        "tags": [
          "Grading"
        ],
        "operationId": "getGradeByIdempotencyKey",
        "summary": "Recover a grading by idempotency key",
        "description": "Performs a read-only, low-bandwidth recovery lookup for an uncertain submission. Send the original `Idempotency-Key` header; no request body or images are needed and no credit is spent. Keys are scoped to the authenticated account and have no time-based TTL, remaining available for the lifetime of the grading record. A match returns the original grading ID, its current status, the account's current credit balance, and `idempotent: true`. A `404 IDEMPOTENCY_KEY_NOT_FOUND` means no grading record exists for the key in the authenticated account; resend the original multipart POST with the same key and images. Retrieve the full result from `GET /api/v1/grade/{id}`. Required scope: `grades:write`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequiredIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The grading recorded for this account and idempotency key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentGradeReference"
                },
                "example": {
                  "id": "d71bb389-...",
                  "status": "pending",
                  "creditsRemaining": 79,
                  "idempotent": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/IdempotencyKeyNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/grades": {
      "get": {
        "tags": [
          "Grading"
        ],
        "operationId": "listGrades",
        "summary": "List your gradings",
        "description": "Returns a paginated list of your gradings, newest first. Required scope: `grades:read`.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Page size (1-100)."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Number of records to skip."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "completed",
                "failed"
              ]
            },
            "description": "Filter by grading status."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of gradings.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/grade/{id}": {
      "get": {
        "tags": [
          "Grading"
        ],
        "operationId": "getGrade",
        "summary": "Get a grading",
        "description": "Returns a single grading. The `results` object is present only when `status` is `completed`; when `status` is `failed`, a `rejection` object explains why (the credit is always refunded). Required scope: `grades:read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Grading ID from the submit response."
          }
        ],
        "responses": {
          "200": {
            "description": "The grading.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/grade/{id}/report": {
      "get": {
        "tags": [
          "Grading"
        ],
        "operationId": "getGradeReport",
        "summary": "Get the report image",
        "description": "Returns the 1200x1800 eBay-ready grading report JPG. On a cache hit returns a `302` redirect to the immutable image URL; on first render streams the JPG directly (`200`, `image/jpeg`). Auto-generates the certificate if missing \u2014 the grading must be `completed`. Required scope: `grades:read`. Clients must follow redirects.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rendered report image.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Redirect to the hosted report image URL.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "Get account info",
        "description": "Returns your account, plan, credit balances, and rate-limit tiers. Required scope: `account:read`.",
        "responses": {
          "200": {
            "description": "Account information.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/grade/validate": {
      "post": {
        "tags": [
          "Grading"
        ],
        "operationId": "validateGrade",
        "summary": "Validate a grade submission (dry run)",
        "description": "Runs the exact same auth, scope, and image validation as `POST /api/v1/grade` but spends NO credit, creates no grading, and uploads nothing. Reports every problem at once (missing/oversized image, wrong content-type) instead of stopping at the first. Use it while building your integration. Required scope: `grades:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "frontImage"
                ],
                "properties": {
                  "frontImage": {
                    "type": "string",
                    "format": "binary",
                    "description": "Front of the card. JPEG, PNG, or WebP. Max 10 MB."
                  },
                  "backImage": {
                    "type": "string",
                    "format": "binary",
                    "description": "Back of the card (optional but recommended for full grading). Same constraints."
                  },
                  "gradingCompany": {
                    "type": "string",
                    "default": "CGI",
                    "description": "Grading standard to grade against."
                  },
                  "cardType": {
                    "type": "string",
                    "default": "Pokemon Card",
                    "description": "Card category hint for identification."
                  },
                  "userNotes": {
                    "type": "string",
                    "description": "Optional free-text note (not stored on the grade)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request is valid (or reports exactly what would be rejected).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "wouldSpendCredits": {
                      "type": "integer",
                      "example": 1
                    },
                    "creditsRemaining": {
                      "type": "integer"
                    },
                    "sufficientCredits": {
                      "type": "boolean"
                    },
                    "request": {
                      "type": "object",
                      "description": "Echo of the parsed request (content-types, sizes, gradingCompany, cardType)."
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request would be rejected \u2014 `error` lists every problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Read-only key (needs grades:write) or plan not API-eligible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your CardGrade API key (prefixed `cgrd_`), sent as `Authorization: Bearer cgrd_...`. Generate one under Settings -> API. Available scopes: `grades:read`, `grades:write`, `account:read`."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Optional account-scoped key. Generate one UUID per logical submission. It has no time-based TTL and remains replayable for the lifetime of the grading record. Reusing it returns the first grading even if later images or parameters differ."
      },
      "RequiredIdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        },
        "description": "The account-scoped key sent with the original submission. A UUID is recommended."
      }
    },
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string"
        },
        "description": "Unique ID for this request, useful in support tickets."
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests allowed in the current 60-second window for this bucket."
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests remaining in the current window."
      },
      "Retry-After": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds to wait before retrying (on 429)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "enum": [
              "UNAUTHORIZED",
              "FORBIDDEN",
              "PLAN_REQUIRED",
              "INSUFFICIENT_SCOPE",
              "RATE_LIMITED",
              "INSUFFICIENT_CREDITS",
              "INVALID_INPUT",
              "IDEMPOTENCY_KEY_NOT_FOUND",
              "NOT_FOUND",
              "INTERNAL_ERROR"
            ],
            "description": "Stable machine-readable code."
          }
        }
      },
      "GradeAccepted": {
        "type": "object",
        "required": [
          "id",
          "status",
          "creditsRemaining"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Grading ID \u2014 poll GET /api/v1/grade/{id}."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "creditsRemaining": {
            "type": "integer"
          },
          "idempotent": {
            "type": "boolean",
            "description": "Present and true when this was an idempotent replay (no credit charged)."
          }
        }
      },
      "IdempotentGradeReference": {
        "type": "object",
        "required": [
          "id",
          "status",
          "creditsRemaining",
          "idempotent"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The original grading ID. Retrieve full results from GET /api/v1/grade/{id}."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "deleted",
              "identified",
              "refunded"
            ],
            "description": "The grading's current status."
          },
          "creditsRemaining": {
            "type": "integer",
            "description": "The account's current credit balance."
          },
          "idempotent": {
            "type": "boolean",
            "const": true,
            "description": "Always true for an idempotent replay or recovery lookup; no credit was charged by this request."
          }
        }
      },
      "GradeSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "cardName": {
            "type": [
              "string",
              "null"
            ]
          },
          "cardYear": {
            "type": [
              "string",
              "null"
            ]
          },
          "cardSet": {
            "type": [
              "string",
              "null"
            ]
          },
          "gradingCompany": {
            "type": [
              "string",
              "null"
            ]
          },
          "overallGrade": {
            "type": [
              "number",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GradeList": {
        "type": "object",
        "required": [
          "gradings",
          "limit",
          "offset"
        ],
        "properties": {
          "gradings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GradeSummary"
            }
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        }
      },
      "GradeDetail": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "cardName": {
            "type": [
              "string",
              "null"
            ]
          },
          "cardYear": {
            "type": [
              "string",
              "null"
            ]
          },
          "cardSet": {
            "type": [
              "string",
              "null"
            ]
          },
          "cardNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "gradingCompany": {
            "type": [
              "string",
              "null"
            ]
          },
          "overallGrade": {
            "type": [
              "number",
              "null"
            ]
          },
          "frontImageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "backImageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "certificateUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Public verification page."
          },
          "reportImageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Rendered report JPG, when generated."
          },
          "shareToken": {
            "type": [
              "string",
              "null"
            ]
          },
          "results": {
            "$ref": "#/components/schemas/GradeResults"
          },
          "rejection": {
            "type": "object",
            "description": "Present only when `status` is `failed`. Explains why the card was rejected, using the same honest reason app users receive (e.g. take it out of the toploader, or a shiny/foil card couldn't be read). The credit is always refunded on a failed grade. Use this to tell the customer how to fix the photo instead of surfacing a bare `failed`.",
            "properties": {
              "code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Machine-readable reason code (e.g. `card_cropped`), or null when no specific code applies."
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Human-readable rejection reason, safe to show the end user."
              }
            }
          }
        }
      },
      "GradeResults": {
        "type": "object",
        "description": "Present only when status is 'completed'.",
        "properties": {
          "centering": {
            "type": [
              "number",
              "null"
            ]
          },
          "corners": {
            "type": [
              "number",
              "null"
            ]
          },
          "edges": {
            "type": [
              "number",
              "null"
            ]
          },
          "surface": {
            "type": [
              "number",
              "null"
            ]
          },
          "overallGrade": {
            "type": [
              "number",
              "null"
            ]
          },
          "psaPrediction": {
            "type": [
              "number",
              "null"
            ]
          },
          "bgsPrediction": {
            "type": [
              "number",
              "null"
            ]
          },
          "cgcPrediction": {
            "type": [
              "number",
              "null"
            ]
          },
          "gradeLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "e.g. 'Gem Mint', 'Mint'."
          },
          "limitingFactor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which axis held the grade back."
          },
          "gradeJustification": {
            "type": [
              "string",
              "null"
            ],
            "description": "AI prose explaining the grade."
          },
          "overallAnalysis": {
            "type": [
              "string",
              "null"
            ]
          },
          "zoneAnalysis": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "frontCornerScores": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "frontEdgeScores": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "backCornerScores": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "backEdgeScores": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "cvMetrics": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Per-zone geometric metrics; centering = {score, left_pct, right_pct, top_pct, bottom_pct}."
          },
          "backCvMetrics": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "frontLlmObservations": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Per-zone defect classes (none|minor|moderate|severe)."
          },
          "backLlmObservations": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "marketValueLow": {
            "type": [
              "number",
              "null"
            ]
          },
          "marketValueHigh": {
            "type": [
              "number",
              "null"
            ]
          },
          "marketAnalysis": {
            "type": [
              "string",
              "null"
            ]
          },
          "zoneCropUrls": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            },
            "description": "Per-corner, per-edge, surface-defect, and full rectified card image URLs."
          },
          "details": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Full raw result blob (back-compat)."
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "id",
          "plan",
          "credits",
          "packCredits",
          "totalCredits",
          "rateLimits"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "plan": {
            "type": [
              "string",
              "null"
            ]
          },
          "planStatus": {
            "type": [
              "string",
              "null"
            ]
          },
          "credits": {
            "type": "integer"
          },
          "packCredits": {
            "type": "integer"
          },
          "totalCredits": {
            "type": "integer"
          },
          "rateLimits": {
            "type": "object",
            "properties": {
              "gradesPerMinute": {
                "type": "integer"
              },
              "readsPerMinute": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, or expired token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Invalid API token",
              "code": "UNAUTHORIZED"
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key lacks the scope required for this operation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "This API key is read-only. Submitting gradings requires a key with the 'grades:write' scope.",
              "code": "INSUFFICIENT_SCOPE"
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "No credits remaining.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Insufficient credits. Purchase more credits to continue grading.",
              "code": "INSUFFICIENT_CREDITS"
            }
          }
        }
      },
      "InvalidInput": {
        "description": "Malformed request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "frontImage is required",
              "code": "INVALID_INPUT"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (or not owned by this account).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Grading not found",
              "code": "NOT_FOUND"
            }
          }
        }
      },
      "IdempotencyKeyNotFound": {
        "description": "No grading for the authenticated account uses this idempotency key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "No grading was found for this Idempotency-Key.",
              "code": "IDEMPOTENCY_KEY_NOT_FOUND"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded for this window.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Rate limit exceeded. Please try again later.",
              "code": "RATE_LIMITED"
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Failed to create grading record",
              "code": "INTERNAL_ERROR"
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "grading.completed": {
      "post": {
        "summary": "Sent to your registered webhook URL when a grade finishes",
        "description": "Register a webhook in your dashboard under Webhooks (any plan with API access). We POST this to your URL the moment a grade completes, so you don't have to poll. Headers: `X-CardGrade-Event: grading.completed` and `X-CardGrade-Signature: sha256=<hmac>` (HMAC-SHA256 of the raw body with your webhook's signing secret). Reply 2xx within 10s.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "example": "grading.completed"
                  },
                  "data": {
                    "$ref": "#/components/schemas/GradeDetail"
                  },
                  "timestamp": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged \u2014 any 2xx marks the delivery successful."
          }
        }
      }
    }
  }
}
