{
  "openapi": "3.1.0",
  "info": {
    "title": "CardGrader.AI Agent API",
    "version": "1.0.0",
    "summary": "Identify, grade, value, and market-analyze trading cards from a photo - one image-based API for Pokemon, sports, and other TCGs.",
    "description": "Multi-service, image-based card API for developers and AI agents. Everything starts from a photo of a card (Pokemon, sports, and other TCGs); you choose which services to run as modules:\n- identify: card identification (name, subject, set, year, number, parallel, print run) - works as a card scanner/recognition API, no catalog lookup needed.\n- grade: AI condition grading - a predicted professional (PSA-style) grade with centering/corners/edges/surface sub-grades. The flagship service, useful for pre-screening cards before paid grading.\n- market: card pricing - raw and graded USD value estimates built from real sold sales data, with a per-grade value spread for grading-ROI math.\n- full: the complete pipeline (identify + grade + market) in one pass.\n\nAlso available as an MCP server at https://cardgrader.ai/mcp for AI agents (the register_agent tool self-onboards a new agent).\n\nHOW TO USE THIS API (agent quickstart):\n1. Register: POST /v1/agents with a name (no human signup needed). You get an API key and free trial credits (1 credit anonymous, 3 if you include contactEmail). The key is shown exactly once - store it.\n2. Submit a scan: POST /v1/scans with a card image (multipart file upload or a public image URL) and the modules you want. Each scan costs credits (see GET /v1/pricing; current defaults: identify=1, grade=1, market=1, full=2).\n3. Poll: GET /v1/scans/{id} until status is \"completed\" (typically tens of seconds). The response contains only the sections for the modules you paid for.\n4. Top up: when you receive 402 insufficient_credits, GET /v1/credits/packs and POST /v1/credits/purchase to get a Stripe Checkout URL a human (or agentic checkout flow) can pay.\n\nAUTHENTICATION: send your key as 'Authorization: Bearer cgk_...' or in the 'X-Api-Key' header. Keys look like 'cgk_' followed by 40 characters.\n\nERROR MODEL: all errors are RFC 9457 application/problem+json with a machine-readable 'code' extension (e.g. insufficient_credits, invalid_modules, idempotency_conflict). The 'type' URI links to human docs at https://cardgrader.ai/api-docs/errors#<code>.\n\nIDEMPOTENCY: POST /v1/scans accepts an Idempotency-Key header (1-64 chars [A-Za-z0-9_-]). Replaying the same key returns the original scan without charging again - always set it when you retry.\n\nRATE LIMITS: registration is limited to 5/hour/IP (plus 3 new agents per IP per 24h); authenticated v1 endpoints are limited to 60 requests/minute per key. 429 responses are problem+json with code rate_limited.\n\nCREDITS: every authenticated response includes an X-Credits-Remaining header so you can track balance without extra calls.",
    "contact": {
      "name": "CardGrader.AI",
      "url": "https://cardgrader.ai/api-docs"
    }
  },
  "servers": [
    {
      "url": "https://cardgrader.ai"
    }
  ],
  "security": [
    { "bearerAuth": [] },
    { "apiKeyHeader": [] }
  ],
  "paths": {
    "/v1/agents": {
      "post": {
        "operationId": "registerAgent",
        "summary": "Register a new agent (no signup, returns API key + trial credits)",
        "description": "Self-serve registration for AI agents. Call this ONCE per agent, store the returned apiKey (it cannot be retrieved again), then reuse it for all future calls.\n\nIncluding 'contactEmail' upgrades the agent to the 'registered' tier and grants 3 trial credits instead of 1 - always include it if you have an operator contact.\n\nAnti-abuse: max 3 agents per IP per 24 hours and 5 registration attempts per hour per IP. A global daily trial-credit pool also applies; if it is exhausted, registration still succeeds but with 0 credits and a 'note' field explaining how to purchase credits.\n\nDo NOT register a new agent to farm trial credits - reuse your existing key and purchase credit packs instead.",
        "security": [],
        "tags": ["Agents"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RegisterAgentRequest" },
              "example": {
                "name": "My Card Bot",
                "contactEmail": "operator@example.com",
                "operatorUrl": "https://example.com"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Agent created. Save apiKey now - it is shown exactly once.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RegisterAgentResponse" },
                "example": {
                  "agentId": 42,
                  "apiKey": "cgk_AbCdEf123456789012345678901234567890XyZw",
                  "tier": "registered",
                  "credits": 3,
                  "docsUrl": "https://cardgrader.ai/api-docs",
                  "message": "Store this key securely; it cannot be retrieved again."
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestProblem" },
          "429": { "$ref": "#/components/responses/RateLimitedProblem" },
          "500": { "$ref": "#/components/responses/ServerProblem" }
        }
      }
    },
    "/v1/agents/me": {
      "get": {
        "operationId": "getAgentMe",
        "summary": "Who am I + current credit balance",
        "description": "Returns the authenticated agent's identity, tier, and current credit balance. Free to call (no credits charged). Useful to check your balance before submitting scans, though every authenticated response also carries an X-Credits-Remaining header.",
        "tags": ["Agents"],
        "responses": {
          "200": {
            "description": "Agent identity and balance.",
            "headers": {
              "X-Credits-Remaining": { "$ref": "#/components/headers/X-Credits-Remaining" },
              "X-RateLimit-Policy": { "$ref": "#/components/headers/X-RateLimit-Policy" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AgentMeResponse" },
                "example": {
                  "agentId": 42,
                  "name": "My Card Bot",
                  "tier": "registered",
                  "credits": 3,
                  "keyPrefix": "cgk_AbCdEf",
                  "createdAt": "2026-06-11T18:00:00Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedProblem" },
          "429": { "$ref": "#/components/responses/RateLimitedProblem" }
        }
      }
    },
    "/v1/scans": {
      "post": {
        "operationId": "createScan",
        "summary": "Submit a card scan (costs credits; async - poll the returned id)",
        "description": "Submit a trading-card photo for any combination of the four services: identification, AI grading, pricing/valuation, and market analysis. Two input styles:\n- multipart/form-data: file field 'front' (required), 'back' (optional), text field 'modules' (comma/space separated). Max 15MB per image, JPEG/PNG.\n- application/json: { frontImageUrl, backImageUrl?, modules } where the image URLs must be publicly fetchable http(s) URLs and modules is an array of strings or a single string.\n\nMODULES and COSTS (live prices at GET /v1/pricing; current defaults in parentheses):\n- \"identify\" (1 credit): card name, subject, set, year, number, parallel, print run.\n- \"grade\" (1 credit): identification + AI condition grade with centering/corners/edges/surface sub-grades.\n- \"market\" (1 credit): identification + raw and graded value estimates (USD) and market insights.\n- \"full\" (2 credits): everything above in one pass - cheaper than combining modules; cannot be combined with other module values.\n- \"deep\" is NOT available via this API (returns 400 deep_unsupported); it requires the CardGrader mobile app's guided multi-angle capture.\n\nCombining modules sums their costs (e.g. [\"identify\",\"grade\"] = 2 credits) - prefer \"full\" when you want everything.\n\nBILLING is debit-first: credits are charged when the scan is accepted and automatically refunded if it cannot be queued. Send an Idempotency-Key header on retries - replaying a key returns the original scan (202) without charging again.\n\nThe response is 202 Accepted with the scan id; poll GET /v1/scans/{id} until status is \"completed\" or \"failed\". Processing typically takes tens of seconds depending on modules.",
        "tags": ["Scans"],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateScanJsonRequest" },
              "example": {
                "frontImageUrl": "https://example.com/card-front.jpg",
                "backImageUrl": "https://example.com/card-back.jpg",
                "modules": ["full"]
              }
            },
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/CreateScanMultipartRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Scan accepted and queued. Poll links.self for the result. A replayed Idempotency-Key also returns 202 with the original scan id and creditsCharged of the original request (no new charge).",
            "headers": {
              "X-Credits-Remaining": { "$ref": "#/components/headers/X-Credits-Remaining" },
              "X-RateLimit-Policy": { "$ref": "#/components/headers/X-RateLimit-Policy" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanAcceptedResponse" },
                "example": {
                  "id": 12345,
                  "status": "queued",
                  "modules": ["full"],
                  "creditsCharged": 2,
                  "creditsRemaining": 1,
                  "links": { "self": "/v1/scans/12345" }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input. Problem codes: missing_front_image, invalid_image_url, invalid_modules, deep_unsupported, image_too_large, invalid_image, invalid_idempotency_key, invalid_body.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedProblem" },
          "402": {
            "description": "Insufficient credits (code insufficient_credits). The problem includes creditsRemaining and purchaseUrl (/v1/credits/packs) - buy a pack and retry.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Problem" },
                    {
                      "type": "object",
                      "properties": {
                        "creditsRemaining": { "type": "integer" },
                        "purchaseUrl": { "type": "string" }
                      }
                    }
                  ]
                },
                "example": {
                  "type": "https://cardgrader.ai/api-docs/errors#insufficient_credits",
                  "title": "Insufficient Credits",
                  "status": 402,
                  "detail": "Your credit balance is too low for this request. Purchase a credit pack to continue.",
                  "code": "insufficient_credits",
                  "creditsRemaining": 0,
                  "purchaseUrl": "/v1/credits/packs"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict (code idempotency_conflict): this Idempotency-Key was used by a request that failed mid-flight and was refunded. Retry with a NEW Idempotency-Key.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimitedProblem" },
          "500": {
            "description": "Server-side failure (codes pricing_unavailable, enqueue_failed). On enqueue_failed your credits were refunded - retry with a new Idempotency-Key.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          }
        }
      }
    },
    "/v1/scans/{id}": {
      "get": {
        "operationId": "getScan",
        "summary": "Poll a scan's status / fetch its results",
        "description": "Returns the scan's current state. Free to call (no credits charged) - poll every few seconds; processing is queue-backed and typically takes 30-120 seconds.\n\n- While queued/processing: { id, status, progressPercent, statusMessage }.\n- On failure: status \"failed\" with a generic error (credits for failed jobs are not auto-refunded; contact support if failures persist).\n- On completion: status \"completed\" plus ONLY the sections you paid for: identification, grading (with subGrades), value (USD estimates + gradedValueSpread), and market.\n\nYou can only fetch scans created by your own agent; anything else is 404.",
        "tags": ["Scans"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scan id returned by POST /v1/scans.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Current scan state. Discriminate on 'status': queued | processing | completed | failed.",
            "headers": {
              "X-Credits-Remaining": { "$ref": "#/components/headers/X-Credits-Remaining" },
              "X-RateLimit-Policy": { "$ref": "#/components/headers/X-RateLimit-Policy" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/ScanInFlight" },
                    { "$ref": "#/components/schemas/ScanFailed" },
                    { "$ref": "#/components/schemas/ScanCompleted" }
                  ]
                },
                "examples": {
                  "processing": {
                    "summary": "Still processing",
                    "value": {
                      "id": 12345,
                      "status": "processing",
                      "progressPercent": 40,
                      "statusMessage": "Grading card..."
                    }
                  },
                  "completed": {
                    "summary": "Completed full scan",
                    "value": {
                      "id": 12345,
                      "status": "completed",
                      "modules": ["full"],
                      "completedAt": "2026-06-11T18:05:00Z",
                      "identification": {
                        "name": "2017 Donruss Optic Patrick Mahomes II #177",
                        "subject": "Patrick Mahomes II",
                        "category": "Sports",
                        "year": "2017",
                        "set": "Donruss Optic",
                        "number": "177",
                        "parallel": "Base",
                        "printRun": ""
                      },
                      "grading": {
                        "grade": 8.5,
                        "predictedGrade": 8.5,
                        "subGrades": { "centering": 9.0, "corners": 8.5, "edges": 8.5, "surface": 8.0 },
                        "summary": "Near mint-mint with minor corner wear.",
                        "justification": "Slight whitening on the back corners; surface clean under glare."
                      },
                      "value": {
                        "rawEstimate": 145.0,
                        "gradedEstimate": 320.0,
                        "currency": "USD",
                        "gradedValueSpread": [
                          { "grade": 8.0, "value": 210.0, "confidence": "High" },
                          { "grade": 9.0, "value": 420.0, "confidence": "Medium" },
                          { "grade": 10.0, "value": 1400.0, "confidence": "Medium" }
                        ]
                      },
                      "market": {
                        "insights": "Strong recent sales; graded premium is significant.",
                        "gradingRecommendation": "grade",
                        "context": "Raw-to-graded spread exceeds grading cost at predicted grade."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedProblem" },
          "404": {
            "description": "No scan with this id exists for this agent (code scan_not_found).",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimitedProblem" }
        }
      }
    },
    "/v1/pricing": {
      "get": {
        "operationId": "getPricing",
        "summary": "Current module costs and credit packs (anonymous, live from config)",
        "description": "The authoritative live price list. Call this before submitting scans if you need exact costs - module credit costs and pack prices are server configuration and can change. Also documents trial credit amounts (1 anonymous / 3 registered). No authentication required, no credits charged.",
        "security": [],
        "tags": ["Pricing"],
        "responses": {
          "200": {
            "description": "Module costs (credits per scan) and purchasable credit packs (USD).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PricingResponse" },
                "example": {
                  "modules": [
                    { "key": "identify", "credits": 1, "description": "Card identification: name, subject, set, year, number, parallel, print run." },
                    { "key": "grade", "credits": 1, "description": "Identification + AI condition grading with sub-grades (centering, corners, edges, surface)." },
                    { "key": "market", "credits": 1, "description": "Identification + value estimates, graded-value spread, and market insights." },
                    { "key": "full", "credits": 2, "description": "The complete fast-scan pipeline: identification, grading, pricing, and market analysis." },
                    { "key": "deep", "credits": 3, "description": "Deep scan (multi-angle capture) - currently requires the CardGrader mobile app; not available via the v1 API." }
                  ],
                  "packs": [
                    { "key": "starter", "displayName": "Starter", "credits": 25, "priceUsd": 5.0 },
                    { "key": "builder", "displayName": "Builder", "credits": 120, "priceUsd": 19.0 },
                    { "key": "scale", "displayName": "Scale", "credits": 400, "priceUsd": 49.0 }
                  ],
                  "trial": { "anonymous": 1, "registered": 3 },
                  "currencyNote": "All prices are in USD. Credits are debited per scan based on the modules requested."
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits/packs": {
      "get": {
        "operationId": "getCreditPacks",
        "summary": "Purchasable credit packs (anonymous)",
        "description": "The credit pack catalog. This is the purchaseUrl that 402 insufficient_credits errors point at. Pick a pack key and POST /v1/credits/purchase (authenticated) to get a Stripe Checkout URL.",
        "security": [],
        "tags": ["Credits"],
        "responses": {
          "200": {
            "description": "Active credit packs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreditPacksResponse" },
                "example": {
                  "packs": [
                    { "key": "starter", "displayName": "Starter", "credits": 25, "priceUsd": 5.0 },
                    { "key": "builder", "displayName": "Builder", "credits": 120, "priceUsd": 19.0 },
                    { "key": "scale", "displayName": "Scale", "credits": 400, "priceUsd": 49.0 }
                  ],
                  "purchase": "POST /v1/credits/purchase with body { \"pack\": \"<key>\" } (API key required) returns a Stripe Checkout URL.",
                  "currencyNote": "All prices are in USD."
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits/purchase": {
      "post": {
        "operationId": "purchaseCredits",
        "summary": "Buy a credit pack (returns a Stripe Checkout URL)",
        "description": "Starts a credit pack purchase for the authenticated agent.\n\nDefault rail: returns a Stripe Checkout URL ({ checkoutUrl }). Hand that URL to a human operator (or an agentic checkout flow) to complete payment in a browser; credits are granted automatically via webhook within seconds of payment. The session expires (expiresAt, ~24h) - if it expires unpaid, simply create a new one.\n\nOptional (preview, feature-flagged server-side): include 'sharedPaymentToken' (Stripe agentic-commerce Shared Payment Token) to pay programmatically without a browser. If the server has SPT disabled you get 501 spt_not_enabled - fall back to the checkoutUrl flow. On SPT success credits are granted inline and the response includes creditsRemaining.",
        "tags": ["Credits"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PurchaseRequest" },
              "example": { "pack": "starter" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created (default rail) OR payment completed (SPT rail). Discriminate on presence of 'checkoutUrl' vs 'paymentIntentId'.",
            "headers": {
              "X-Credits-Remaining": { "$ref": "#/components/headers/X-Credits-Remaining" },
              "X-RateLimit-Policy": { "$ref": "#/components/headers/X-RateLimit-Policy" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/PurchaseCheckoutResponse" },
                    { "$ref": "#/components/schemas/PurchaseSptResponse" }
                  ]
                },
                "examples": {
                  "checkout": {
                    "summary": "Default Checkout rail",
                    "value": {
                      "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_...",
                      "pack": "starter",
                      "credits": 25,
                      "priceUsd": 5.0,
                      "expiresAt": "2026-06-12T18:00:00Z"
                    }
                  },
                  "spt": {
                    "summary": "Shared Payment Token rail (preview)",
                    "value": {
                      "status": "succeeded",
                      "pack": "starter",
                      "credits": 25,
                      "priceUsd": 5.0,
                      "creditsRemaining": 26,
                      "paymentIntentId": "pi_3..."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown or missing pack (code invalid_pack; detail lists valid keys).",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedProblem" },
          "402": {
            "description": "SPT charge did not complete (code spt_payment_incomplete). No credits were granted.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimitedProblem" },
          "501": {
            "description": "sharedPaymentToken was sent but SPT is not enabled on this server (code spt_not_enabled). Omit sharedPaymentToken to use the Checkout URL flow.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          },
          "502": {
            "description": "Stripe error (codes stripe_error, spt_stripe_error). No credits were granted; retry later.",
            "content": {
              "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key as a bearer token: 'Authorization: Bearer cgk_...'. Obtain a key via POST /v1/agents."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Alternative to the Authorization header: 'X-Api-Key: cgk_...'."
      }
    },
    "headers": {
      "X-Credits-Remaining": {
        "description": "The agent's credit balance after this request.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Policy": {
        "description": "Name of the rate-limit policy applied to this request.",
        "schema": { "type": "string" }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Client-chosen retry-safety key, 1-64 chars of [A-Za-z0-9_-]. Replaying the same key returns the original scan without charging again. Always set this when you may retry.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,64}$"
        }
      }
    },
    "responses": {
      "UnauthorizedProblem": {
        "description": "Missing, invalid, revoked, or disabled API key (code invalid_api_key). Register via POST /v1/agents if you have no key.",
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      },
      "RateLimitedProblem": {
        "description": "Rate limit exceeded (code rate_limited). Back off and retry shortly.",
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      },
      "BadRequestProblem": {
        "description": "Invalid request. The problem 'code' identifies the field/rule that failed.",
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      },
      "ServerProblem": {
        "description": "Unexpected server-side failure. Safe to retry.",
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 'code' is the stable machine-readable error identifier; 'type' links to its documentation.",
        "properties": {
          "type": { "type": "string", "description": "Doc URI: https://cardgrader.ai/api-docs/errors#<code>" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "code": { "type": "string", "description": "Stable machine-readable error code, e.g. insufficient_credits." }
        },
        "required": ["type", "title", "status", "detail", "code"]
      },
      "RegisterAgentRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100,
            "description": "Display name for this agent (required)."
          },
          "contactEmail": {
            "type": ["string", "null"],
            "maxLength": 256,
            "description": "Operator contact email. Providing it upgrades the agent to tier 'registered' and grants 3 trial credits instead of 1."
          },
          "operatorUrl": {
            "type": ["string", "null"],
            "maxLength": 512,
            "description": "Absolute http(s) URL identifying the operator/product behind this agent."
          }
        },
        "required": ["name"]
      },
      "RegisterAgentResponse": {
        "type": "object",
        "properties": {
          "agentId": { "type": "integer" },
          "apiKey": { "type": "string", "description": "Plaintext API key (cgk_...). Shown exactly once - store it securely." },
          "tier": { "type": "string", "enum": ["anonymous", "registered"] },
          "credits": { "type": "integer", "description": "Trial credits granted (0 if the daily trial pool is exhausted - see 'note')." },
          "docsUrl": { "type": "string" },
          "message": { "type": "string" },
          "note": { "type": ["string", "null"], "description": "Present only when the daily trial-credit pool was exhausted." }
        },
        "required": ["agentId", "apiKey", "tier", "credits", "docsUrl", "message"]
      },
      "AgentMeResponse": {
        "type": "object",
        "properties": {
          "agentId": { "type": "integer" },
          "name": { "type": "string" },
          "tier": { "type": "string", "enum": ["anonymous", "registered", "paid"] },
          "credits": { "type": "integer" },
          "keyPrefix": { "type": ["string", "null"], "description": "First characters of the active key, for display/logging." },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["agentId", "name", "tier", "credits", "createdAt"]
      },
      "ModulesValue": {
        "description": "Modules to run: an array from [\"identify\",\"grade\",\"market\"], or \"full\" (alone). A single comma/space-separated string is also accepted. \"deep\" is rejected with 400 deep_unsupported.",
        "oneOf": [
          {
            "type": "array",
            "items": { "type": "string", "enum": ["identify", "grade", "market", "full"] }
          },
          { "type": "string" }
        ]
      },
      "CreateScanJsonRequest": {
        "type": "object",
        "properties": {
          "frontImageUrl": {
            "type": "string",
            "description": "Publicly fetchable absolute http(s) URL of the card front photo (required)."
          },
          "backImageUrl": {
            "type": "string",
            "description": "Publicly fetchable absolute http(s) URL of the card back photo (required - the pipeline analyzes both sides)."
          },
          "modules": { "$ref": "#/components/schemas/ModulesValue" }
        },
        "required": ["frontImageUrl", "backImageUrl", "modules"]
      },
      "CreateScanMultipartRequest": {
        "type": "object",
        "properties": {
          "front": {
            "type": "string",
            "format": "binary",
            "description": "Card front image file (JPEG/PNG, max 15MB). Required."
          },
          "back": {
            "type": "string",
            "format": "binary",
            "description": "Card back image file (JPEG/PNG, max 15MB). Required - the pipeline analyzes both sides."
          },
          "modules": {
            "type": "string",
            "description": "Comma/space-separated module list, e.g. \"full\" or \"identify,grade\"."
          }
        },
        "required": ["front", "back", "modules"]
      },
      "ScanAcceptedResponse": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Scan id - poll GET /v1/scans/{id}." },
          "status": { "type": "string", "enum": ["queued"] },
          "modules": { "type": "array", "items": { "type": "string" } },
          "creditsCharged": { "type": "integer" },
          "creditsRemaining": { "type": "integer" },
          "links": {
            "type": "object",
            "properties": { "self": { "type": "string" } },
            "required": ["self"]
          }
        },
        "required": ["id", "status", "modules", "creditsCharged", "creditsRemaining", "links"]
      },
      "ScanInFlight": {
        "type": "object",
        "description": "Scan still queued or processing - poll again in a few seconds.",
        "properties": {
          "id": { "type": "integer" },
          "status": { "type": "string", "enum": ["queued", "processing"] },
          "progressPercent": { "type": "integer" },
          "statusMessage": { "type": ["string", "null"] }
        },
        "required": ["id", "status", "progressPercent"]
      },
      "ScanFailed": {
        "type": "object",
        "description": "Processing failed. The error is intentionally generic.",
        "properties": {
          "id": { "type": "integer" },
          "status": { "type": "string", "enum": ["failed"] },
          "progressPercent": { "type": "integer" },
          "statusMessage": { "type": "string" },
          "error": { "type": "string" }
        },
        "required": ["id", "status", "error"]
      },
      "ScanCompleted": {
        "type": "object",
        "description": "Completed scan. Contains ONLY the sections for the modules that were requested and paid for.",
        "properties": {
          "id": { "type": "integer" },
          "status": { "type": "string", "enum": ["completed"] },
          "modules": { "type": "array", "items": { "type": "string" } },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "identification": { "$ref": "#/components/schemas/Identification" },
          "grading": { "$ref": "#/components/schemas/Grading" },
          "value": { "$ref": "#/components/schemas/ValueEstimate" },
          "market": { "$ref": "#/components/schemas/MarketAnalysis" }
        },
        "required": ["id", "status", "modules"]
      },
      "Identification": {
        "type": "object",
        "description": "Card identification (present when 'identify', 'grade', 'market', or 'full' was requested).",
        "properties": {
          "name": { "type": "string", "description": "Full card name/title." },
          "subject": { "type": "string", "description": "Player/character on the card." },
          "category": { "type": "string", "description": "e.g. Pokemon, Sports." },
          "year": { "type": "string" },
          "set": { "type": "string" },
          "number": { "type": "string" },
          "parallel": { "type": "string", "description": "Parallel/variant name; empty or 'Base' for the base card." },
          "printRun": { "type": "string", "description": "Serial-numbered print run if visible, e.g. '/99'." }
        }
      },
      "Grading": {
        "type": "object",
        "description": "AI condition grading on the 1-10 scale (present when 'grade' or 'full' was requested).",
        "properties": {
          "grade": { "type": "number", "description": "Overall AI grade (1-10)." },
          "predictedGrade": { "type": ["number", "null"], "description": "Predicted professional grade." },
          "subGrades": {
            "type": "object",
            "properties": {
              "centering": { "type": "number" },
              "corners": { "type": "number" },
              "edges": { "type": "number" },
              "surface": { "type": "number" }
            }
          },
          "summary": { "type": "string" },
          "justification": { "type": "string" }
        }
      },
      "ValueEstimate": {
        "type": "object",
        "description": "Market value estimates in USD (present when 'market' or 'full' was requested).",
        "properties": {
          "rawEstimate": { "type": "number", "description": "Estimated value ungraded (USD)." },
          "gradedEstimate": { "type": "number", "description": "Estimated value at the predicted grade (USD)." },
          "currency": { "type": "string", "enum": ["USD"] },
          "gradedValueSpread": {
            "type": "array",
            "description": "Estimated value at each professional grade level.",
            "items": {
              "type": "object",
              "properties": {
                "grade": { "type": "number" },
                "value": { "type": "number" },
                "confidence": { "type": "string", "description": "High | Medium | Low." }
              }
            }
          }
        }
      },
      "MarketAnalysis": {
        "type": "object",
        "description": "Market insights (present when 'market' or 'full' was requested).",
        "properties": {
          "insights": { "type": "string" },
          "gradingRecommendation": { "type": "string", "description": "Whether professionally grading this card looks worthwhile (e.g. neutral)." },
          "context": { "type": ["string", "null"] }
        }
      },
      "PricingResponse": {
        "type": "object",
        "properties": {
          "modules": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": { "type": "string" },
                "credits": { "type": "integer" },
                "description": { "type": "string" }
              }
            }
          },
          "packs": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CreditPack" }
          },
          "trial": {
            "type": "object",
            "properties": {
              "anonymous": { "type": "integer" },
              "registered": { "type": "integer" }
            }
          },
          "currencyNote": { "type": "string" }
        }
      },
      "CreditPack": {
        "type": "object",
        "properties": {
          "key": { "type": "string", "description": "Pack key to use in POST /v1/credits/purchase." },
          "displayName": { "type": "string" },
          "credits": { "type": "integer" },
          "priceUsd": { "type": "number" }
        }
      },
      "CreditPacksResponse": {
        "type": "object",
        "properties": {
          "packs": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CreditPack" }
          },
          "purchase": { "type": "string", "description": "How to purchase." },
          "currencyNote": { "type": "string" }
        }
      },
      "PurchaseRequest": {
        "type": "object",
        "properties": {
          "pack": {
            "type": "string",
            "description": "Pack key from GET /v1/credits/packs (e.g. starter, builder, scale)."
          },
          "sharedPaymentToken": {
            "type": ["string", "null"],
            "description": "Optional Stripe agentic-commerce Shared Payment Token (preview). Requires SPT to be enabled server-side; otherwise 501 spt_not_enabled."
          }
        },
        "required": ["pack"]
      },
      "PurchaseCheckoutResponse": {
        "type": "object",
        "description": "Default rail: complete payment at checkoutUrl; credits are granted automatically after payment.",
        "properties": {
          "checkoutUrl": { "type": "string", "description": "Stripe Checkout URL - open in a browser to pay." },
          "pack": { "type": "string" },
          "credits": { "type": "integer" },
          "priceUsd": { "type": "number" },
          "expiresAt": { "type": "string", "format": "date-time", "description": "When the checkout session expires (~24h)." }
        },
        "required": ["checkoutUrl", "pack", "credits", "priceUsd"]
      },
      "PurchaseSptResponse": {
        "type": "object",
        "description": "SPT rail: payment already completed; credits granted inline.",
        "properties": {
          "status": { "type": "string", "enum": ["succeeded"] },
          "pack": { "type": "string" },
          "credits": { "type": "integer" },
          "priceUsd": { "type": "number" },
          "creditsRemaining": { "type": "integer" },
          "paymentIntentId": { "type": "string" }
        },
        "required": ["status", "pack", "credits", "priceUsd", "creditsRemaining", "paymentIntentId"]
      }
    }
  },
  "tags": [
    { "name": "Agents", "description": "Registration and identity. Start here: POST /v1/agents gets you a key and trial credits." },
    { "name": "Scans", "description": "The core image-based services: submit a card photo and poll for identification, AI grading, pricing, and market analysis results. Costs credits." },
    { "name": "Pricing", "description": "Live module costs and pack prices." },
    { "name": "Credits", "description": "Buy credits via Stripe Checkout (or Shared Payment Token, preview)." }
  ]
}
