{
  "openapi": "3.1.0",
  "info": {
    "title": "Triremes API",
    "version": "1.0.0",
    "description": "Search Triremes' problems and websites with poor SEO / AEO, and buy leads, idea reports and websites, paid from prepaid credits. Access is a monthly subscription; keys are made on https://triremes.com/account/api. See https://triremes.com/api, including its terms. Limits: 60 requests a minute per key; 120 a minute per client address, with or without a key; 10 a minute per address with a key that is not valid; past any of them, 429 rate_limited with Retry-After. GET /problems and GET /website-gaps answer 400 invalid_parameter for a parameter they do not take; slugs, site ids and purchase ids are checked for their form (400 invalid_parameter) before they are looked up."
  },
  "servers": [{"url": "https://triremes.com/api/v1"}],
  "security": [{"key": []}],
  "components": {
    "securitySchemes": {
      "key": {"type": "http", "scheme": "bearer", "description": "An API key, trm_live_..., sent as Authorization: Bearer."}
    },
    "parameters": {
      "slug": {"name": "slug", "in": "path", "required": true, "schema": {"type": "string"}, "description": "A problem's slug, or its public id."}
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {"type": "string", "enum": ["invalid_parameter", "invalid_json", "invalid_key", "insufficient_credit", "subscription_inactive",
                "not_found", "kind_unavailable", "idempotency_key_reused", "gone", "unsupported_media_type", "rate_limited", "daily_spend_cap",
                "server_error"]},
              "message": {"type": "string"}
            }
          }
        }
      },
      "Problem": {
        "type": "object",
        "required": ["slug", "url", "name", "title", "tier", "tier_label", "kind", "businesses", "reports", "last_reported", "summary",
          "price_cents", "price", "already_owned"],
        "properties": {
          "slug": {"type": "string"},
          "url": {"type": "string", "format": "uri"},
          "name": {"type": "string"},
          "title": {"type": "string"},
          "tier": {"type": "string", "enum": ["A", "B", "C"]},
          "tier_label": {"type": "string"},
          "kind": {"type": "string", "enum": ["lead", "idea"], "description": "What the problem sells now."},
          "businesses": {"type": "integer", "description": "How many businesses a lead has; 0 for an idea report."},
          "reports": {"type": "integer"},
          "first_seen": {"type": "string", "format": "date-time"},
          "first_reported": {"type": "string", "format": "date-time"},
          "last_reported": {"type": ["string", "null"], "format": "date-time"},
          "summary": {"type": "string", "description": "A lead's full summary; an idea report's first sentence."},
          "price_cents": {"type": "integer", "description": "What it costs now: a lead, its businesses × the price per business, at least the minimum; an idea report, its tier's price. POST /purchases charges this."},
          "price": {"type": "string"},
          "already_owned": {"type": "boolean"},
          "products": {"type": "array", "items": {"type": "string"}},
          "related": {"type": "array", "items": {"type": "object", "properties": {"slug": {"type": "string"}, "name": {"type": "string"}}}}
        }
      },
      "Site": {
        "type": "object",
        "description": "A website on sale, as its page shows it before it is bought: never which business it is.",
        "required": ["id", "url", "title", "category", "region", "city", "country", "gaps", "measured_at", "price_cents", "price", "already_owned"],
        "properties": {
          "id": {"type": "string", "pattern": "^site-[0-9]{1,18}$"},
          "url": {"type": "string", "format": "uri"},
          "title": {"type": "string"},
          "category": {"type": "string", "description": "What the business does: a category, a NAICS title or code, or \"A business\"."},
          "region": {"type": "string"},
          "city": {"type": "string"},
          "country": {"type": "string"},
          "gaps": {"type": "array", "items": {"type": "string"}, "description": "The gaps found, by name."},
          "measured_at": {"type": "string", "format": "date-time"},
          "price_cents": {"type": "integer"},
          "price": {"type": "string"},
          "already_owned": {"type": "boolean"}
        }
      },
      "Purchase": {
        "type": "object",
        "required": ["id", "item", "title", "kind", "tier", "amount_cents", "paid_with", "purchased_at"],
        "properties": {
          "id": {"type": "string"},
          "item": {"type": "string", "description": "The problem's slug now, empty when it is no longer listed; a website's id."},
          "title": {"type": "string"},
          "kind": {"type": "string", "enum": ["lead", "idea", "website_gaps"]},
          "tier": {"type": "string", "description": "A problem's tier; empty for a website."},
          "amount_cents": {"type": "integer"},
          "paid_with": {"type": "string", "enum": ["credits", "card"]},
          "purchased_at": {"type": "string", "format": "date-time"},
          "charged": {"type": "boolean", "description": "POST /purchases only: whether credit was spent."},
          "balance_cents": {"type": "integer", "description": "POST /purchases only: the balance after it."},
          "content": {
            "description": "Exactly what the purchase's page shows.",
            "oneOf": [
              {
                "type": "object",
                "required": ["businesses"],
                "properties": {
                  "businesses": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "business": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"},
                        "published_on_url": {"type": "string"},
                        "suggested_opening": {"type": "string", "description": "An AI-suggested opening line for this lead's problem; empty while it has none."}
                      }
                    }
                  }
                }
              },
              {
                "type": "object",
                "required": ["report"],
                "properties": {"report": {"type": ["object", "null"]}, "note": {"type": "string"}}
              },
              {
                "type": "object",
                "required": ["sites"],
                "properties": {
                  "sites": {
                    "type": "array",
                    "description": "The website as it was measured, the same copy a purchase on the site keeps.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {"type": "string"}, "business": {"type": "string"}, "website": {"type": "string"},
                        "contact_page": {"type": "string"}, "category": {"type": "string"}, "region": {"type": "string"}, "city": {"type": "string"},
                        "country": {"type": "string"}, "measured_at": {"type": "string", "format": "date-time"},
                        "gaps": {"type": "integer"},
                        "findings": {"type": "array", "items": {"type": "object", "properties": {
                          "check": {"type": "string"}, "gap": {"type": "boolean"}, "fact": {"type": "string"},
                          "value": {"type": "string"}, "threshold": {"type": "string"}}}},
                        "contacts": {"type": "array", "description": "The email addresses and phone numbers the business publishes, each with where: page is the page it was published on, or organization the domain's registrant for source registration. Absent when none was found.",
                          "items": {"type": "object", "properties": {
                          "kind": {"type": "string", "enum": ["email", "phone"]}, "value": {"type": "string"},
                          "source": {"type": "string", "enum": ["homepage", "contact_page", "structured_data", "registration"]},
                          "page": {"type": "string"}, "organization": {"type": "string"}}}}
                      }
                    }
                  }
                }
              }
            ]
          }
        }
      }
    },
    "responses": {
      "Error": {"description": "An error.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    }
  },
  "paths": {
    "/problems": {
      "get": {
        "summary": "Search the public problems.",
        "parameters": [
          {"name": "q", "in": "query", "schema": {"type": "string", "maxLength": 200}},
          {"name": "tier", "in": "query", "schema": {"type": "string", "enum": ["A", "B", "C"]}},
          {"name": "has_businesses", "in": "query", "schema": {"type": "boolean"}},
          {"name": "since", "in": "query", "schema": {"type": "string"}, "description": "A date (2026-10-17) or an RFC 3339 time: problems first seen then or later."},
          {"name": "cursor", "in": "query", "schema": {"type": "string"}, "description": "The next_cursor of the page before."},
          {"name": "limit", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25}}
        ],
        "responses": {
          "200": {
            "description": "One page.",
            "content": {"application/json": {"schema": {"type": "object", "required": ["problems", "next_cursor"], "properties": {
              "problems": {"type": "array", "items": {"$ref": "#/components/schemas/Problem"}},
              "next_cursor": {"type": ["string", "null"]}}}}}
          },
          "400": {"$ref": "#/components/responses/Error"}, "401": {"$ref": "#/components/responses/Error"},
          "403": {"$ref": "#/components/responses/Error"}, "429": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/problems/{slug}": {
      "get": {
        "summary": "One problem, as its page shows it.",
        "parameters": [{"$ref": "#/components/parameters/slug"}],
        "responses": {
          "200": {"description": "The problem.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "400": {"$ref": "#/components/responses/Error"}, "404": {"$ref": "#/components/responses/Error"},
          "410": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/website-gaps": {
      "get": {
        "summary": "The websites with poor SEO / AEO on sale, with their gaps.",
        "parameters": [
          {"name": "category", "in": "query", "schema": {"type": "string", "maxLength": 100}, "description": "A business category, a NAICS title or a NAICS code."},
          {"name": "region", "in": "query", "schema": {"type": "string", "maxLength": 100}},
          {"name": "city", "in": "query", "schema": {"type": "string", "maxLength": 100}},
          {"name": "country", "in": "query", "schema": {"type": "string", "maxLength": 100}},
          {"name": "since", "in": "query", "schema": {"type": "string"}, "description": "A date (2026-10-17) or an RFC 3339 time: websites measured then or later."},
          {"name": "cursor", "in": "query", "schema": {"type": "string"}, "description": "The next_cursor of the page before."},
          {"name": "limit", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 25}}
        ],
        "responses": {
          "200": {
            "description": "One page.",
            "content": {"application/json": {"schema": {"type": "object", "required": ["sites", "next_cursor"], "properties": {
              "sites": {"type": "array", "items": {"$ref": "#/components/schemas/Site"}},
              "next_cursor": {"type": ["string", "null"]}}}}}
          },
          "400": {"$ref": "#/components/responses/Error"}, "401": {"$ref": "#/components/responses/Error"},
          "403": {"$ref": "#/components/responses/Error"}, "429": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/website-gaps/{id}": {
      "get": {
        "summary": "One website, as its page shows it.",
        "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "pattern": "^site-[0-9]{1,18}$"}}],
        "responses": {
          "200": {"description": "The website.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Site"}}}},
          "400": {"$ref": "#/components/responses/Error"}, "404": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/purchases": {
      "post": {
        "summary": "Buy a problem's lead or idea report, or a website with poor SEO / AEO, with credits.",
        "parameters": [{"name": "Idempotency-Key", "in": "header", "schema": {"type": "string", "maxLength": 255, "pattern": "^[!-~]*$"},
          "description": "The same key always returns the same purchase, without charge."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["item", "kind"],
          "additionalProperties": false, "properties": {
          "item": {"type": "string", "maxLength": 200, "description": "The problem's slug or public id; a website's id."},
          "kind": {"type": "string", "enum": ["lead", "idea", "website_gaps"]}}}}}},
        "responses": {
          "201": {"description": "Bought: credit spent.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Purchase"}}}},
          "200": {"description": "Already owned, or the same Idempotency-Key: no charge.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Purchase"}}}},
          "400": {"$ref": "#/components/responses/Error"}, "402": {"$ref": "#/components/responses/Error"},
          "409": {"$ref": "#/components/responses/Error"}, "415": {"$ref": "#/components/responses/Error"},
          "429": {"$ref": "#/components/responses/Error"}
        }
      },
      "get": {
        "summary": "Every purchase made with the account's address, newest first, without content.",
        "responses": {"200": {"description": "The purchases.", "content": {"application/json": {"schema": {"type": "object", "properties": {
          "purchases": {"type": "array", "items": {"$ref": "#/components/schemas/Purchase"}}}}}}}}
      }
    },
    "/purchases/{id}": {
      "get": {
        "summary": "One purchase, with its content.",
        "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}],
        "responses": {
          "200": {"description": "The purchase.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Purchase"}}}},
          "400": {"$ref": "#/components/responses/Error"}, "404": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/balance": {
      "get": {
        "summary": "The credit, today's spending and the subscription.",
        "responses": {"200": {"description": "The balance.", "content": {"application/json": {"schema": {"type": "object", "properties": {
          "balance_cents": {"type": "integer"}, "balance": {"type": "string"}, "spent_today_cents": {"type": "integer"},
          "daily_spend_cap_cents": {"type": "integer"},
          "subscription": {"type": "object", "properties": {"status": {"type": "string"}, "cancel_at_period_end": {"type": "boolean"}}}}}}}}}
      }
    }
  }
}
