{
  "openapi": "3.1.0",
  "info": {
    "title": "Torn Intel Public API",
    "version": "1.0.0",
    "summary": "Unauthenticated read-only endpoints of the Torn Intel faction tracker.",
    "description": "Torn Intel is a faction intelligence platform for the browser game Torn City.\n\nThis document covers only the endpoints that answer an anonymous request. The rest of\nthe API is scoped to a signed-in player through the `torn-intel-session` cookie and is\ndescribed at https://torn-intel.com/developers.\n\nPlease cache responses and respect the documented cache windows: the data behind these\nendpoints is refreshed on a schedule, so polling faster returns identical bytes.\n\n## Versioning and deprecation\n\n- The canonical form of every public endpoint is versioned: `https://torn-intel.com/api/v1/public/stats`. Integrate against that.\n- The unversioned path is a permanent alias of the same handler, kept working indefinitely.\n- Every API response carries `X-API-Version: 1`.\n- A breaking change ships as a new version prefix (`/api/v2/...`). A v1 response shape is never mutated in place; adding a field is not breaking, removing or retyping one is.\n- A version scheduled for removal answers with `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers plus a `Link rel=\"deprecation\"`, for at least 180 days before it stops.\n- Nothing is currently deprecated.\n\n## Rate limits\n\n- 120 requests per 60 seconds per client, across all public endpoints combined.\n- Every response carries `RateLimit-Policy` and `RateLimit` (draft-ietf-httpapi-ratelimit-headers-09), plus the `RateLimit-Limit`/`-Remaining`/`-Reset` triplet for older clients.\n- Exceeding it returns `429` as `application/problem+json` with the `quota-exceeded` problem type and a `Retry-After` header. Wait that long; do not retry immediately.\n\n## Errors\n\nEvery error is JSON, never an HTML page, and carries a stable machine-readable `code`\nplus a `resolution` describing what to do about it. Branch on `code`, not on the\nhuman-readable `error` or `message` strings.\n\nNot affiliated with Torn.com.",
    "contact": {
      "name": "Torn Intel",
      "url": "https://torn-intel.com/about#contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://torn-intel.com/terms"
    },
    "termsOfService": "https://torn-intel.com/terms"
  },
  "servers": [
    {
      "url": "https://torn-intel.com",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/v1/public/foreign-stock": {
      "get": {
        "operationId": "getForeignStock",
        "summary": "Live abroad stock for every travel destination",
        "description": "Current foreign stock, grouped by country. Each item carries quantity, buy cost, Torn market value and the resulting profit per item. Restock predictions are not exposed here: they are available to signed-in users only.\n\nCache: public, s-maxage=20, stale-while-revalidate=40.\n\nAlso reachable at the unversioned alias `/api/public/foreign-stock`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "generatedAt": "2026-08-25T09:41:02.113Z",
                  "source": "torntools",
                  "countries": [
                    {
                      "country": "mex",
                      "updatedAt": "2026-08-25T09:40:47.000Z",
                      "items": [
                        {
                          "itemId": 206,
                          "itemName": "Xanax",
                          "category": "Drug",
                          "quantity": 45,
                          "cost": 250000,
                          "marketValue": 820000,
                          "profitPerItem": 570000
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/public/foreign-stock/history": {
      "get": {
        "operationId": "getForeignStockHistory",
        "summary": "Observed stock time-series for one item in one country",
        "description": "Quantity and price as observed over a rolling window, oldest point first. Observations only — no forecast.\n\nCache: public, s-maxage=30, stale-while-revalidate=60.\n\nAlso reachable at the unversioned alias `/api/public/foreign-stock/history`.",
        "security": [],
        "parameters": [
          {
            "name": "itemId",
            "in": "query",
            "required": true,
            "description": "Torn item ID.",
            "schema": {
              "type": "integer"
            },
            "example": 206
          },
          {
            "name": "country",
            "in": "query",
            "required": true,
            "description": "Destination code as returned by the stock endpoint.",
            "schema": {
              "type": "string"
            },
            "example": "mex"
          },
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "description": "Window length in hours. Clamped to 1–48. Defaults to 24.",
            "schema": {
              "type": "integer"
            },
            "example": 24
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "country": "mex",
                  "itemId": 206,
                  "hours": 24,
                  "points": [
                    {
                      "t": "2026-08-24T10:00:11.000Z",
                      "quantity": 120,
                      "cost": 250000,
                      "marketValue": 818000
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "itemId or country missing, or itemId not a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/public/stats": {
      "get": {
        "operationId": "getSiteStats",
        "summary": "Platform coverage counters",
        "description": "How many Torn players are indexed, how many factions are watched, and how many have registered.\n\nCache: revalidated every 300s.\n\nAlso reachable at the unversioned alias `/api/public/stats`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "playersIndexed": 3184220,
                  "watchedFactions": 1462,
                  "factionsRegistered": 318
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/public/forum-stats": {
      "get": {
        "operationId": "getForumStats",
        "summary": "Counters formatted for forum and Discord posts",
        "description": "Registered factions, Pro factions, watched factions and spied players, as four integers.\n\nCache: public, max-age=3600, s-maxage=3600.\n\nAlso reachable at the unversioned alias `/api/public/forum-stats`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "factions": 318,
                  "proFactions": 74,
                  "watchedFactions": 1462,
                  "spiedPlayers": 88140
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/public/registered": {
      "get": {
        "operationId": "getPlayerRegistered",
        "summary": "Whether a Torn player has a Torn Intel account",
        "description": "Returns a single boolean keyed by Torn player ID. Exposes no personal data, which is why it is unauthenticated and open to cross-origin calls from the in-game script.\n\nCache: not cached.\n\nCross-origin: allowed from any origin.\n\nAlso reachable at the unversioned alias `/api/public/registered`.",
        "security": [],
        "parameters": [
          {
            "name": "playerId",
            "in": "query",
            "required": true,
            "description": "Torn player ID (XID).",
            "schema": {
              "type": "integer"
            },
            "example": 2353554
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "registered": true
                }
              }
            }
          },
          "400": {
            "description": "playerId missing or not a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/news/public": {
      "get": {
        "operationId": "getPublicNews",
        "summary": "Published changelog and announcement entries",
        "description": "Newest first. Each article carries id, title, content, tag, optional image URL and publish date.\n\nCache: not cached.\n\nAlso reachable at the unversioned alias `/api/news/public`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "articles": [
                    {
                      "id": 12,
                      "title": "Departure planner rewrite",
                      "content": "…",
                      "tag": "feature",
                      "image_url": null,
                      "published_at": "2026-08-24T18:02:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/stats-badge": {
      "get": {
        "operationId": "getStatsBadge",
        "summary": "Embeddable SVG badge — coverage counters",
        "description": "A 440×56 SVG showing registered factions, watched factions and spied players. Safe to hotlink.\n\nCache: not cached.\n\nAlso reachable at the unversioned alias `/api/stats-badge`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/intel-badge": {
      "get": {
        "operationId": "getIntelBadge",
        "summary": "Embeddable SVG badge — factions and peak players indexed",
        "description": "A 380×56 SVG showing how many factions and how many players the index covers.\n\nCache: not cached.\n\nAlso reachable at the unversioned alias `/api/intel-badge`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/public/pro-badge": {
      "get": {
        "operationId": "getProBadge",
        "summary": "Embeddable SVG badge — Pro faction count",
        "description": "A single-line SVG reading \"N Pro factions trust Torn Intel\". Width adapts to the number.\n\nCache: public, max-age=300, s-maxage=300, stale-while-revalidate=600.\n\nAlso reachable at the unversioned alias `/api/public/pro-badge`.",
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Standard error body. `code` is the stable machine-readable identifier; branch on it rather than on the prose fields.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable summary."
          },
          "code": {
            "type": "string",
            "enum": [
              "not_found",
              "method_not_allowed",
              "invalid_request",
              "missing_parameter",
              "invalid_parameter",
              "unauthorized",
              "forbidden",
              "rate_limited",
              "not_acceptable",
              "maintenance",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong."
          },
          "resolution": {
            "type": "string",
            "description": "What the caller should do about it."
          },
          "status": {
            "type": "integer"
          },
          "documentation_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "error",
          "code",
          "message",
          "resolution",
          "status",
          "documentation_url"
        ]
      },
      "QuotaExceeded": {
        "type": "object",
        "description": "RFC 9457 problem document for a rate-limit rejection.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "violated-policies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "rate_limited"
          },
          "message": {
            "type": "string"
          },
          "resolution": {
            "type": "string"
          },
          "documentation_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ]
      }
    },
    "responses": {
      "NotFound": {
        "description": "No endpoint exists at this path. Not transient — do not retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Quota exceeded (120 requests / 60s per client).",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit": {
            "description": "Structured limit state, e.g. `\"public\";r=0;t=42`.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/QuotaExceeded"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "torn-intel-session",
        "description": "JWT session cookie issued by POST /api/auth/login. Required by every endpoint outside this document; none of the endpoints listed here use it."
      }
    }
  }
}