{
  "openapi": "3.0.3",
  "info": {
    "title": "CompsAPI",
    "version": "1.4.0",
    "description": "Real eBay sold prices as JSON. Up to 3 years of history, real accepted Best Offer prices, and a trading card layer for graded cards and comps. Docs: https://compsapi.com/docs · Keys: https://app.compsapi.com",
    "contact": {
      "name": "CompsAPI support",
      "url": "https://app.compsapi.com/support",
      "email": "keys@compsapi.com"
    },
    "termsOfService": "https://compsapi.com/terms",
    "x-logo": {
      "url": "https://compsapi.com/apple-touch-icon.png",
      "altText": "CompsAPI"
    }
  },
  "servers": [
    {
      "url": "https://api.compsapi.com"
    }
  ],
  "security": [
    {
      "apiKey": []
    },
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "parameters": {
      "q": {
        "name": "q",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "maxLength": 300
        },
        "description": "Search terms. eBay matches every word in the title."
      },
      "days": {
        "name": "days",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 1095,
          "default": 90
        },
        "description": "How far back to search, up to 3 years."
      },
      "size": {
        "name": "size",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 10,
          "maximum": 240,
          "default": 200
        },
        "description": "Results per page."
      },
      "condition": {
        "name": "condition",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "new, open_box, refurbished, used or parts. Comma separate to combine."
      },
      "best_offer": {
        "name": "best_offer",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "1",
            "only",
            "verify"
          ]
        },
        "description": "1 adds the best_offer flag with real accepted prices; only returns accepted offers exclusively; verify also reads the public sold page and adds best_offer_confidence, asking_price and asking_gap_pct, because eBay's own accepted offer filter misses some negotiated sales."
      },
      "cards": {
        "name": "cards",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        },
        "description": "Adds the card object to every item without filtering."
      },
      "grader": {
        "name": "grader",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "PSA, BGS, CGC, SGC, TAG, ACE and others. Comma separate to combine."
      },
      "grade": {
        "name": "grade",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Grade as text, e.g. 10 or 9.5. Comma separate to combine."
      },
      "graded": {
        "name": "graded",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ]
        },
        "description": "Only slabs, or only raw cards."
      },
      "lang": {
        "name": "lang",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "en, ja, zh, ko, de, fr, it, es, pt. Comma separate to combine."
      },
      "first_edition": {
        "name": "first_edition",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ]
        }
      },
      "exclude": {
        "name": "exclude",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Flags to drop, e.g. junk or lot,sealed. none keeps everything."
      },
      "stats": {
        "name": "stats",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        },
        "description": "Adds price stats for the filtered results."
      },
      "start": {
        "name": "start",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date",
          "example": "2024-12-01"
        },
        "description": "First day of an absolute window, written YYYY-MM-DD. Use start and end to ask for a fixed historical period instead of a lookback from today, for example start=2024-12-01&end=2025-12-01. Inclusive. Given on its own, the window runs to now. A start more than 1095 days ago is refused; on a plan with less history the start is pulled forward to that plan's limit and plan_limit says so. Slicing a long period into several short windows is faster and more reliable than walking deep pages."
      },
      "end": {
        "name": "end",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date",
          "example": "2025-12-01"
        },
        "description": "Last day of an absolute window, written YYYY-MM-DD. Inclusive, so the whole of that day counts, and it never runs past today. Given on its own, the window starts days before it. The answer carries window with the period actually served."
      },
      "require": {
        "name": "require",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Keep only sales whose title holds every one of these words or phrases, comma separated, for example require=lebron james or require=lebron james,red disco. Matching ignores case and punctuation and lands on whole words, so 2017 matches a 2017-18 title and ron jam matches nothing. Up to 6 terms. This is how you search broadly and then insist on the one thing that identifies your item. It can only remove rows, never find new ones, so the answer carries title_filter with the count before it ran and how many it removed. It runs on the page already retrieved, so on a broad query widen size or walk pages to see more."
      },
      "print_run": {
        "name": "print_run",
        "in": "query",
        "schema": {
          "type": "string",
          "example": "125"
        },
        "description": "The serial number a card is limited to, read from the title. A number keeps only that print run, for example print_run=125 for a /125 parallel. numbered keeps every serial numbered card. any filters nothing and just labels the rows. All three add print_run to every item. #098/125 reads as 125 and not as 98, a bare /85 reads as 85, and a season like 2025/26, a card number like #100, a 100 card lot and a date are all left alone. A print run is not an identity: a /125 parallel of the same player exists in several seasons at very different prices, so keep the year in q and use this to separate parallels within it. Reported in title_filter the same way as require."
      }
    },
    "schemas": {
      "Card": {
        "type": "object",
        "properties": {
          "graded": {
            "type": "boolean",
            "nullable": true
          },
          "grader": {
            "type": "string",
            "nullable": true
          },
          "grade": {
            "type": "string",
            "nullable": true
          },
          "grade_label": {
            "type": "string",
            "nullable": true
          },
          "card_number": {
            "type": "string",
            "nullable": true
          },
          "year": {
            "type": "integer",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "first_edition": {
            "type": "boolean",
            "nullable": true
          },
          "flags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Item": {
        "type": "object",
        "properties": {
          "item_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "last_sold": {
            "type": "string",
            "format": "date-time"
          },
          "price": {
            "type": "number",
            "description": "Average price actually paid per unit, USD. Accepted best offers show the real offer price."
          },
          "units_sold": {
            "type": "integer"
          },
          "total_sales": {
            "type": "number"
          },
          "format": {
            "type": "string",
            "enum": [
              "FIXED_PRICE",
              "AUCTION"
            ]
          },
          "bids": {
            "type": "integer"
          },
          "us_sale": {
            "type": "boolean",
            "description": "true when the sale was made on eBay US, false when it was made on another eBay site. The default search merges eBay US with international coverage, so this separates them. Which other site a false row came from is not reported, because eBay does not expose it per item. With site pinned, every row is true or every row is false."
          },
          "native_price": {
            "type": "number",
            "nullable": true,
            "description": "Only with native_price=1. The amount in the currency the sale actually happened in. price is always USD; this is the untouched figure. null when it could not be confirmed, typically a listing that sold several units at different prices."
          },
          "native_currency": {
            "type": "string",
            "nullable": true,
            "description": "Only with native_price=1. ISO 4217 code for native_price, for example GBP, EUR, AUD."
          },
          "best_offer": {
            "type": "boolean",
            "nullable": true,
            "description": "Only present with the best_offer parameter. true means the sale was identified as an accepted offer; false means it was not identified as one, which is not the same as a full price sale."
          },
          "condition": {
            "type": "string",
            "description": "Only present with a single condition value."
          },
          "shipping_cost": {
            "type": "number",
            "nullable": true
          },
          "free_shipping_pct": {
            "type": "number"
          },
          "url": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "card": {
            "$ref": "#/components/schemas/Card"
          },
          "best_offer_confidence": {
            "type": "string",
            "enum": [
              "confirmed",
              "likely",
              "unlikely",
              "unknown"
            ],
            "description": "Only with best_offer=verify. confirmed: eBay identifies the sale as an accepted offer. likely: the listing accepted offers and the sale closed materially below the asking price. unlikely: neither holds. unknown: the sale was not on the public pages we read, or another marketplace was pinned, so no comparison was possible."
          },
          "asking_price": {
            "type": "number",
            "nullable": true,
            "description": "Only with best_offer=verify. The price eBay's public sold page displays for this listing."
          },
          "asking_gap_pct": {
            "type": "number",
            "nullable": true,
            "description": "Only with best_offer=verify. How far the asking price sits above what the sale closed at, as a percentage of the amount paid. Negative when the public page reads low."
          }
        }
      },
      "Stats": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer"
          },
          "used_for_stats": {
            "type": "integer"
          },
          "median": {
            "type": "number"
          },
          "mean": {
            "type": "number"
          },
          "p25": {
            "type": "number"
          },
          "p75": {
            "type": "number"
          },
          "min": {
            "type": "number"
          },
          "max": {
            "type": "number"
          },
          "outliers_removed": {
            "type": "integer"
          },
          "newest_sale": {
            "type": "string",
            "format": "date-time"
          },
          "oldest_sale": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SoldResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "query": {
            "type": "string"
          },
          "page": {
            "type": "integer"
          },
          "ms": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "no_results": {
            "type": "boolean"
          },
          "next_page": {
            "type": "boolean"
          },
          "days": {
            "type": "integer"
          },
          "size": {
            "type": "integer"
          },
          "cached": {
            "type": "boolean"
          },
          "cached_age_s": {
            "type": "integer"
          },
          "source": {
            "type": "string"
          },
          "filters": {
            "type": "object"
          },
          "count_before_filters": {
            "type": "integer"
          },
          "stats": {
            "$ref": "#/components/schemas/Stats"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Missing or invalid parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/sold": {
      "get": {
        "summary": "Sold listings for a search, newest sale first",
        "parameters": [
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/days"
          },
          {
            "$ref": "#/components/parameters/start"
          },
          {
            "$ref": "#/components/parameters/end"
          },
          {
            "$ref": "#/components/parameters/size"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 1
            },
            "description": "Page number. 1 to 200 on a paid plan, 1 to 50 on the free plan. Walk pages in order: each page is served from where the previous one stopped, so a deep page asked for on its own has to rebuild the whole walk and can time out. A walk stays resumable for 4 hours."
          },
          {
            "$ref": "#/components/parameters/condition"
          },
          {
            "$ref": "#/components/parameters/best_offer"
          },
          {
            "$ref": "#/components/parameters/cards"
          },
          {
            "$ref": "#/components/parameters/grader"
          },
          {
            "$ref": "#/components/parameters/grade"
          },
          {
            "$ref": "#/components/parameters/graded"
          },
          {
            "$ref": "#/components/parameters/lang"
          },
          {
            "$ref": "#/components/parameters/first_edition"
          },
          {
            "$ref": "#/components/parameters/exclude"
          },
          {
            "$ref": "#/components/parameters/require"
          },
          {
            "$ref": "#/components/parameters/print_run"
          },
          {
            "$ref": "#/components/parameters/stats"
          },
          {
            "name": "site",
            "in": "query",
            "description": "Pin one eBay marketplace, returning only sales made on that site. Omit it for the default, which is eBay US plus international coverage merged. A pin means that site alone, not listings a buyer there could see. All prices are USD whichever you pick: eBay converts before we receive them, so a pin changes which sales you get, never the currency.",
            "schema": {
              "type": "string",
              "enum": [
                "US",
                "UK",
                "DE",
                "FR",
                "IT",
                "ES",
                "CA",
                "AU"
              ]
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "eBay category id(s), comma separated, e.g. 31387 for wristwatches.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+(,\\d+)*$"
            }
          },
          {
            "name": "min_price",
            "in": "query",
            "description": "Keep only sales at or above this sold price (applied to the returned page).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "max_price",
            "in": "query",
            "description": "Keep only sales at or below this sold price (applied to the returned page).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "native_price",
            "in": "query",
            "description": "1 adds native_price and native_currency: the amount in the currency the buyer actually paid. eBay US sales are free (already USD); each non US sale costs one extra read, so it is capped per request and native_price_info reports coverage.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sold listings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SoldResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/comps": {
      "get": {
        "summary": "Price stats plus the 10 newest matching sales (card comps)",
        "description": "Searches the newest sales, removes junk listings, always parses the card layer, and returns stats plus the 10 newest matching sales. Not pageable.",
        "parameters": [
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/days"
          },
          {
            "$ref": "#/components/parameters/start"
          },
          {
            "$ref": "#/components/parameters/end"
          },
          {
            "$ref": "#/components/parameters/size"
          },
          {
            "$ref": "#/components/parameters/condition"
          },
          {
            "$ref": "#/components/parameters/best_offer"
          },
          {
            "$ref": "#/components/parameters/grader"
          },
          {
            "$ref": "#/components/parameters/grade"
          },
          {
            "$ref": "#/components/parameters/graded"
          },
          {
            "$ref": "#/components/parameters/lang"
          },
          {
            "$ref": "#/components/parameters/first_edition"
          },
          {
            "$ref": "#/components/parameters/exclude"
          },
          {
            "$ref": "#/components/parameters/require"
          },
          {
            "$ref": "#/components/parameters/print_run"
          },
          {
            "name": "native_price",
            "in": "query",
            "description": "1 adds native_price and native_currency: the amount in the currency the buyer actually paid. eBay US sales are free (already USD); each non US sale costs one extra read, so it is capped per request and native_price_info reports coverage.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stats and newest sales.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SoldResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "summary": "Usage for the current UTC day (does not count against limits)",
        "responses": {
          "200": {
            "description": "Usage counters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "day_utc": {
                      "type": "string"
                    },
                    "used": {
                      "type": "integer"
                    },
                    "daily_cap": {
                      "type": "integer"
                    },
                    "trial_hours": {
                      "type": "integer",
                      "nullable": true
                    },
                    "trial_started": {
                      "type": "string",
                      "nullable": true
                    },
                    "expires": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "summary": "Service status",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up."
          }
        }
      }
    }
  }
}
