{
  "openapi": "3.1.0",
  "info": {
    "title": "VendBro API",
    "description": "Public API for VendBro — card scans, sessions, inventory, and recognition. All endpoints require an `Authorization: Bearer vb_live_...` API key. Generate keys at https://app.vendbro.com → Profile → API Keys. Scope enforcement is per-route; see `security` blocks below.",
    "contact": {
      "name": "VendBro",
      "url": "https://vendbro.com"
    },
    "license": {
      "name": "Apache-2.0"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.vendbro.com/v1",
      "description": "Production"
    },
    {
      "url": "http://localhost:3002/v1",
      "description": "Local dev"
    }
  ],
  "paths": {
    "/lookup": {
      "get": {
        "tags": [
          "recognition"
        ],
        "description": "Look up a card by name (+ optional number/game). Returns the same shape as /scan but without recognition/OCR metadata. This is a free read — it does NOT count against the user's scan quota, so use it instead of /scan when you already know a card's identity. Any valid API key works.",
        "operationId": "v1_lookup_doc",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Card name (fuzzy-matched).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "number",
            "in": "query",
            "description": "Set number (sharpens the match).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "game",
            "in": "query",
            "description": "'pokemon' (default) or 'mtg'.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matched candidates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "502": {
            "description": "AI service unreachable"
          }
        }
      }
    },
    "/user/inventory": {
      "get": {
        "tags": [
          "inventory"
        ],
        "description": "List up to 2000 inventory items, newest first. Requires scope `inventory:read`.",
        "operationId": "v1_list_inventory_doc",
        "responses": {
          "200": {
            "description": "Inventory snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/V1InventoryRow"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or insufficient scope"
          }
        }
      }
    },
    "/user/scans": {
      "get": {
        "tags": [
          "scans"
        ],
        "description": "List up to 500 most-recent scans, newest first. Requires scope `scan:read`.",
        "operationId": "v1_list_scans_doc",
        "responses": {
          "200": {
            "description": "Recent scans",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/V1ScanRow"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing key, bad key, or key lacks scan:read"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "post": {
        "tags": [
          "scans"
        ],
        "description": "Record a single scan. Prefer /user/scans/bulk for batches. The scan's card_id should come from /lookup or /scan. Requires scope `scan:write`.",
        "operationId": "v1_add_scan_doc",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1BulkScanEntry"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Inserted OK",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "400": {
            "description": "Field too long or invalid"
          },
          "401": {
            "description": "Missing or insufficient scope"
          }
        }
      }
    },
    "/user/scans/bulk": {
      "post": {
        "tags": [
          "scans"
        ],
        "description": "Batch-insert scans (up to 1000). Used by list-mode saves and CSV imports. Unknown session_ids in the payload are silently dropped rather than failing the whole request. Requires scope `scan:write`.",
        "operationId": "v1_add_scans_bulk_doc",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1BulkScanRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Inserted OK",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "400": {
            "description": "Too many scans or field too long"
          },
          "401": {
            "description": "Missing or insufficient scope"
          }
        }
      }
    },
    "/user/sessions": {
      "get": {
        "tags": [
          "sessions"
        ],
        "description": "List up to 50 most-recent sessions. Requires scope `sessions:read`.",
        "operationId": "v1_list_sessions_doc",
        "responses": {
          "200": {
            "description": "Recent sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/V1SessionRow"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or insufficient scope"
          }
        }
      },
      "post": {
        "tags": [
          "sessions"
        ],
        "description": "Create or update a named session. Client generates the id (format: `sess_YYYYMMDDHHMMSS` is conventional but any short string works). Requires scope `sessions:write`.",
        "operationId": "v1_upsert_session_doc",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1UpsertSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Upserted OK",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "401": {
            "description": "Missing or insufficient scope"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CardCandidate": {
        "type": "object",
        "description": "A card candidate returned by the AI service.",
        "required": [
          "id",
          "name",
          "set_name",
          "set_id",
          "number",
          "prices",
          "score"
        ],
        "properties": {
          "colors": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "hp": {
            "type": [
              "object",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "illustrator": {
            "type": [
              "string",
              "null"
            ]
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "image_large": {
            "type": [
              "string",
              "null"
            ]
          },
          "layout": {
            "type": [
              "string",
              "null"
            ]
          },
          "mana_cost": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "oracle_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "prices": {
            "type": "object"
          },
          "rarity": {
            "type": [
              "string",
              "null"
            ]
          },
          "score": {
            "type": "integer",
            "format": "int32"
          },
          "set_id": {
            "type": "string"
          },
          "set_name": {
            "type": "string"
          },
          "tcgplayer_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "type_line": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RecognitionInfo": {
        "type": "object",
        "required": [
          "confidence",
          "language",
          "game",
          "raw_ocr_texts"
        ],
        "properties": {
          "card_name_ocr": {
            "type": [
              "string",
              "null"
            ]
          },
          "confidence": {
            "type": "number",
            "format": "float"
          },
          "game": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "raw_ocr_texts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scan_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "set_number_ocr": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ScanResponse": {
        "type": "object",
        "description": "Full scan response returned to the frontend.",
        "required": [
          "recognition",
          "candidates"
        ],
        "properties": {
          "candidates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CardCandidate"
            }
          },
          "recognition": {
            "$ref": "#/components/schemas/RecognitionInfo"
          }
        }
      },
      "V1BulkScanEntry": {
        "type": "object",
        "description": "Single entry in a bulk scan-save request.",
        "required": [
          "card_id",
          "card_name"
        ],
        "properties": {
          "card_id": {
            "type": "string"
          },
          "card_name": {
            "type": "string"
          },
          "card_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "condition": {
            "type": [
              "string",
              "null"
            ]
          },
          "game": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "price_at_scan": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          },
          "scan_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "set_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "sold_price": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          },
          "variant": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "V1BulkScanRequest": {
        "type": "object",
        "required": [
          "scans"
        ],
        "properties": {
          "scans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1BulkScanEntry"
            }
          }
        }
      },
      "V1InventoryRow": {
        "type": "object",
        "description": "One inventory item (a card the user is holding for sale).",
        "required": [
          "sku",
          "game",
          "card_id",
          "card_name",
          "set_name",
          "set_code",
          "card_number",
          "condition",
          "market_price",
          "created_at"
        ],
        "properties": {
          "acquired_price": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          },
          "binder_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "binder_page": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "binder_slot": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "card_id": {
            "type": "string"
          },
          "card_name": {
            "type": "string"
          },
          "card_number": {
            "type": "string"
          },
          "condition": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "game": {
            "type": "string"
          },
          "location_assigned_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "market_price": {
            "type": "number",
            "format": "float"
          },
          "set_code": {
            "type": "string"
          },
          "set_name": {
            "type": "string"
          },
          "sku": {
            "type": "string"
          },
          "sold_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "sold_price": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          }
        }
      },
      "V1ScanRow": {
        "type": "object",
        "description": "A scan row in the user's history.",
        "required": [
          "id",
          "card_id",
          "card_name",
          "created_at"
        ],
        "properties": {
          "card_id": {
            "type": "string"
          },
          "card_name": {
            "type": "string"
          },
          "card_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "condition": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "game": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "price_at_scan": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          },
          "scan_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "set_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "sold_price": {
            "type": [
              "number",
              "null"
            ],
            "format": "float"
          },
          "variant": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "V1SessionRow": {
        "type": "object",
        "description": "A named scan session (bucket for grouping scans).",
        "required": [
          "id",
          "label",
          "started_at"
        ],
        "properties": {
          "ended_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "started_at": {
            "type": "string"
          }
        }
      },
      "V1UpsertSessionRequest": {
        "type": "object",
        "required": [
          "id",
          "label"
        ],
        "properties": {
          "ended_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "bearer_auth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "vb_live_<hex>",
        "description": "VendBro API key. Generate at https://app.vendbro.com → Profile → API Keys. Scoped per-route; see each endpoint's description."
      }
    }
  },
  "security": [
    {
      "bearer_auth": []
    }
  ]
}