{
  "openapi": "3.1.0",
  "info": {
    "title": "CardGrade API",
    "version": "1.0.0",
    "summary": "AI trading-card pre-grading as a REST API.",
    "description": "Submit a card image and receive a full AI grade: overall grade, per-zone corner/edge/surface scores, centering metrics, AI prose analysis, market value, 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 ACTIVE Pro or Business subscription (trials, which have status `trialing`, do not qualify).\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\nPass an `Idempotency-Key` header (any UUID) to make retries safe: if a request times out and you resend it with the same key, you receive the original grading back (HTTP 200, `idempotent: true`) instead of being charged a second credit.\n\nThe grade runs asynchronously \u2014 poll `GET /api/v1/grade/{id}` until `status` is `completed`.",
        "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)."
                  }
                }
              }
            }
          }
        },
        "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 the grade for this Idempotency-Key already exists; no credit was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeAccepted"
                },
                "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/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)."
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "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"
        },
        "description": "Optional unique key (e.g. a UUID). Retrying a submission with the same key returns the original grading instead of charging a second credit."
      }
    },
    "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",
              "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)."
          }
        }
      },
      "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"
            }
          }
        }
      },
      "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."
          }
        }
      }
    }
  }
}
