{
  "openapi": "3.1.0",
  "info": {
    "title": "CardGrader.AI Agent API",
    "version": "1.1.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 derived from cached sales data; coverage and freshness vary, 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.\n\nCOMMERCIAL GRADING: USD250/month for 5,000 or USD449/month for 10,000 complete front+back grades. POST /v1/grades uses a verified seven-day unlimited grading trial or paid monthly allowance and returns numeric grading fields directly, with no identification/value/market output. Monthly grades and prepaid API credits are separate. /v1/scans continues using prepaid credits. Subscription signup and management at https://cardgrader.ai/api-pricing. No rollover; cancel at period end; plan changes at renewal.\nTRIAL ONBOARDING: For commercial-only accounts, POST /v1/agents with name, contactEmail and commercialOnly=true (no prepaid demo grant). POST /v1/subscriptions/checkout with a plan returns hosted Stripe checkout. The owner must enter payment details and accept automatic billing: new accounts receive seven days unlimited grading only, then USD250/5000 or USD449/10000 per month. Cancel before trial end to avoid the first charge. One trial per API account. Check /v1/subscriptions/me after checkout; unlimitedGrading=true and trialEndsAtUtc confirm active access. An unpaid or expired trial stops grading access. Creating a checkout link does not begin the trial. MCP start_grading_trial combines registration and checkout preparation. Never send card credentials to an agent.",
    "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"
                }
              }
            }
          }
        }
      }
    },
    "/v1/subscriptions/checkout": {
      "post": {
        "operationId": "createCommercialSubscriptionCheckout",
        "summary": "Hosted subscription Checkout URL. No allowance until Stripe confirms payment. Contact email required.",
        "tags": [
          "Commercial subscriptions"
        ],
        "responses": {
          "200": {
            "description": "Checkout URL, portal URL, or scheduled plan and effectiveAtUtc."
          },
          "400": {
            "description": "Invalid plan or missing contact email."
          },
          "409": {
            "description": "Existing subscription, checkout or conflicting plan change."
          },
          "503": {
            "description": "Billing unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommercialPlanRequest"
              }
            }
          }
        },
        "description": "Prepare hosted Stripe checkout for the owner to confirm. New accounts get a payment-method-required 7-day unlimited grading-only trial, followed automatically by the monthly plan. Returning subscribers pay immediately. Cancel before trial end to avoid the first charge. Link creation does not start access or charge a card. Verify balances after checkout."
      }
    },
    "/v1/subscriptions/portal": {
      "post": {
        "operationId": "createCommercialBillingPortal",
        "summary": "Manage payment method and cancel at period end; API customer is separate from consumer subscriptions.",
        "tags": [
          "Commercial subscriptions"
        ],
        "responses": {
          "200": {
            "description": "Checkout URL, portal URL, or scheduled plan and effectiveAtUtc."
          },
          "400": {
            "description": "Invalid plan or missing contact email."
          },
          "409": {
            "description": "Existing subscription, checkout or conflicting plan change."
          },
          "503": {
            "description": "Billing unavailable."
          }
        }
      }
    },
    "/v1/subscriptions/change-plan": {
      "post": {
        "operationId": "scheduleCommercialPlanChange",
        "summary": "Schedule tier change at next renewal; current allowance is unchanged.",
        "tags": [
          "Commercial subscriptions"
        ],
        "responses": {
          "200": {
            "description": "Checkout URL, portal URL, or scheduled plan and effectiveAtUtc."
          },
          "400": {
            "description": "Invalid plan or missing contact email."
          },
          "409": {
            "description": "Existing subscription, checkout or conflicting plan change."
          },
          "503": {
            "description": "Billing unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommercialPlanRequest"
              }
            }
          }
        }
      }
    },
    "/v1/subscriptions/plans": {
      "get": {
        "operationId": "listCommercialPlans",
        "summary": "Monthly grading-only plans and checkout availability",
        "security": [],
        "tags": [
          "Commercial subscriptions"
        ],
        "responses": {
          "200": {
            "description": "Plan list; both photos of one card count as one grade."
          }
        }
      }
    },
    "/v1/subscriptions/me": {
      "get": {
        "operationId": "getCommercialBalances",
        "summary": "Separate prepaid API credit and subscription grade balances",
        "tags": [
          "Commercial subscriptions"
        ],
        "responses": {
          "200": {
            "description": "Current balances and paid period dates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommercialBalances"
                }
              }
            }
          }
        }
      }
    },
    "/v1/grades": {
      "post": {
        "operationId": "gradeCommercialCard",
        "summary": "Grade both sides of one card using one monthly grade; grading only",
        "description": "Returns directly (allow up to 65 seconds). Required front/back JPEG or PNG up to15 MB each. At most one in-flight grade per API account. Does not run identification or market services. No prepaid fallback. Failed grades are refunded to their original period. Repeat a completed request with the same key AND identical photo bytes to replay without charging again. Use a new key after a refunded failure. During a verified seven-day trial there is no grade-count cap and no paid allowance deduction. At the trial deadline, unlimited access ends regardless of webhook delay. A successful payment is required for the monthly quota.",
        "tags": [
          "Commercial grading"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommercialGradeRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "front",
                  "back"
                ],
                "properties": {
                  "front": {
                    "type": "string",
                    "format": "binary"
                  },
                  "back": {
                    "type": "string",
                    "format": "binary"
                  },
                  "modules": {
                    "const": "grade"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completed grade; one monthly grade charged for the photo pair.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommercialGradeResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid/missing photos or Idempotency-Key."
          },
          "402": {
            "description": "subscription_required or grading_allowance_exhausted; prepaid credits unchanged."
          },
          "403": {
            "description": "subscription_grading_only for non-grade modules."
          },
          "409": {
            "description": "Idempotency key conflict/in-flight/refunded request."
          },
          "429": {
            "description": "grade_in_flight or rate_limited; wait and retry."
          },
          "503": {
            "description": "Grader unavailable/busy; allowance refunded when reserved."
          }
        }
      }
    },
    "/v1/grades/{id}": {
      "get": {
        "operationId": "getCommercialGrade",
        "summary": "Get an owned commercial grade result",
        "tags": [
          "Commercial grading"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Numeric suffix from grade_123; follow links.self."
          }
        ],
        "responses": {
          "200": {
            "description": "Saved grade result, processing or refunded status."
          },
          "404": {
            "description": "Missing/foreign grade ID."
          }
        }
      }
    },
    "/v1/subscriptions/cancel": {
      "post": {
        "operationId": "cancelCommercialSubscription",
        "summary": "Cancel future charges; current trial/paid-period access continues until its deadline",
        "responses": {
          "200": {
            "description": "Cancellation scheduled"
          },
          "404": {
            "description": "No renewable subscription"
          }
        },
        "tags": [
          "Commercial subscriptions"
        ]
      }
    }
  },
  "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."
          },
          "commercialOnly": {
            "type": "boolean",
            "default": false,
            "description": "Commercial grading signup: no prepaid demo credits. A trial starts only after owner completes Stripe checkout."
          }
        },
        "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"
          },
          "balances": {
            "$ref": "#/components/schemas/CommercialBalances"
          }
        },
        "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"
        ]
      },
      "CommercialBalances": {
        "type": "object",
        "properties": {
          "prepaidCredits": {
            "type": "integer"
          },
          "gradingSubscription": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "plan": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "gradingOnly": {
                "const": true
              },
              "includedGrades": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Null while unlimitedGrading=true; numeric paid-period allowance otherwise."
              },
              "gradesRemaining": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Null while unlimitedGrading=true; numeric paid-period allowance otherwise."
              },
              "periodStartUtc": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "periodEndUtc": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cancelAtPeriodEnd": {
                "type": "boolean"
              },
              "rollover": {
                "const": false
              },
              "unlimitedGrading": {
                "type": "boolean",
                "description": "True only during the verified, unexpired commercial trial. Grade-count unlimited; normal rate/concurrency controls apply."
              },
              "trialEndsAtUtc": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "firstChargeAtUtc": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "trialEligible": {
            "type": "boolean"
          }
        }
      },
      "CommercialPlanRequest": {
        "type": "object",
        "required": [
          "plan"
        ],
        "properties": {
          "plan": {
            "enum": [
              "grading_5000",
              "grading_10000"
            ]
          }
        }
      },
      "CommercialGradeRequest": {
        "type": "object",
        "required": [
          "frontImageUrl",
          "backImageUrl"
        ],
        "properties": {
          "frontImageUrl": {
            "type": "string",
            "format": "uri"
          },
          "backImageUrl": {
            "type": "string",
            "format": "uri"
          },
          "modules": {
            "type": [
              "string",
              "array"
            ],
            "description": "Omit or specify only grade. All other modules are rejected."
          }
        }
      },
      "CommercialGradeResult": {
        "type": "object",
        "required": [
          "id",
          "status",
          "grading",
          "billingSource",
          "gradesCharged",
          "creditsCharged"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^grade_[0-9]+$"
          },
          "status": {
            "const": "completed"
          },
          "billingSource": {
            "enum": [
              "subscription",
              "trial"
            ]
          },
          "gradesCharged": {
            "enum": [
              0,
              1
            ],
            "description": "0 during unlimited trial; 1 for a paid-period grade. Prepaid credits are never spent."
          },
          "creditsCharged": {
            "const": 0
          },
          "grading": {
            "type": "object",
            "description": "Overall grade, centering/corners/edges/surface subGrades, and front/back numeric corner/edge/surface scores. No identification or free text. Unavailable centering or per-side cells are null, not zero-quality scores."
          },
          "links": {
            "type": "object",
            "properties": {
              "self": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  },
  "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)."
    }
  ]
}
