{
  "openapi": "3.1.0",
  "info": {
    "title": "Tweet Archive API",
    "description": "Archive a tweet (X post) permanently, free, no API key required. Submits a Wayback Machine capture (plus a screenshot) that survives even if the tweet is later deleted from X. See https://x-dot-com-archive.vercel.app/agents for full docs.",
    "version": "1.0.0"
  },
  "servers": [{ "url": "https://x-dot-com-archive.vercel.app" }],
  "security": [],
  "paths": {
    "/api/v1/tweets": {
      "post": {
        "operationId": "archiveTweets",
        "summary": "Submit 1-10 tweets to archive",
        "description": "Archiving happens in the background after this returns; snapshots may still be empty right away. Poll GET /api/v1/tweets/{id} until a snapshot appears (usually within about a minute).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ArchiveRequest" },
              "examples": {
                "single": { "value": { "urls": ["https://x.com/user/status/1234567890"] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per submitted URL, same order as the input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      { "$ref": "#/components/schemas/ArchivedTweet" },
                      { "$ref": "#/components/schemas/ArchiveError" }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Whole-request error (bad JSON, urls not 1-10 strings, wrong field types).",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } }
            }
          }
        }
      }
    },
    "/api/v1/tweets/{id}": {
      "get": {
        "operationId": "getTweetArchiveStatus",
        "summary": "Check a tweet's archive status",
        "description": "Read-only, no side effects; safe to poll repeatedly.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The tweet's numeric ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Current archive status for this tweet.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ArchivedTweet" } }
            }
          },
          "404": {
            "description": "This ID has never been submitted.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } }
            }
          }
        }
      }
    },
    "/api/v1/tweets/{id}/thread/check": {
      "post": {
        "operationId": "checkTweetThread",
        "summary": "Free preview of a tweet's thread/reply context",
        "description": "Ancestors (what this tweet is replying to) are always free and always returned. If x_bearer_token is given, this also pays for exactly one X API search call to find the author's latest reply in the conversation, then walks backward for free to build a full preview of the forward thread -- not just a count -- with a real cost estimate for upgrading whichever of those are individually truncated. Never spends more than that one search call; upgrading truncated text only happens in a separate call to /thread/ingest.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The tweet's numeric ID."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/ThreadCheckRequest" } }
          }
        },
        "responses": {
          "200": {
            "description": "Ancestors plus, if a token was given, a forward-thread preview.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ThreadCheckResponse" } }
            }
          },
          "404": {
            "description": "This ID has never been submitted and isn't fetchable.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } }
            }
          }
        }
      }
    },
    "/api/v1/tweets/{id}/thread/ingest": {
      "post": {
        "operationId": "ingestTweetThread",
        "summary": "Archive a tweet's thread/reply context",
        "description": "Calling this endpoint at all is the confirmation -- there's no separate confirm flag. Ancestors always ingest (free, regardless of token). If x_bearer_token is given, also discovers (or reuses a fresh cached check for) the forward thread and, unless upgrade_truncated is false or max_cost_usd is exceeded, upgrades each truncated tweet to full text one at a time. Every tweet in the final set gets a Wayback capture scheduled the same way POST /api/v1/tweets does.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The tweet's numeric ID."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/ThreadIngestRequest" } }
          }
        },
        "responses": {
          "200": {
            "description": "Ancestors plus, if forward ingestion ran, the archived forward tweets.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ThreadIngestResponse" } }
            }
          },
          "404": {
            "description": "This ID has never been submitted and isn't fetchable.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ArchiveRequest": {
        "type": "object",
        "required": ["urls"],
        "properties": {
          "urls": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": { "type": "string" },
            "description": "Tweet URLs (e.g. https://x.com/user/status/123) or bare numeric IDs."
          },
          "x_bearer_token": {
            "type": "string",
            "description": "Your own X API Bearer Token, for full-length tweets. Only spent when the free syndication API's result is actually truncated or fails outright; a normal-length tweet costs nothing even if supplied. This service never provides or pays for one."
          },
          "agent": {
            "type": "string",
            "description": "A name to self-identify as (e.g. \"my-bot/1.0\"), recorded on the archive for attribution. Falls back to the User-Agent header if omitted."
          }
        }
      },
      "ArchivedTweet": {
        "type": "object",
        "required": ["id", "status", "tweet", "truncated", "archive_url", "first_archived_at", "removed_at", "removed_snapshot", "snapshots"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["live", "removed"] },
          "tweet": {
            "description": "Raw syndication-API tweet payload, or null once removed from X.",
            "type": ["object", "null"]
          },
          "truncated": {
            "type": "boolean",
            "description": "True if the stored text is a cut-off preview (over ~280 chars, no x_bearer_token supplied or it didn't help)."
          },
          "note": {
            "type": "string",
            "description": "Present only when truncated, explains how to get the full text."
          },
          "archive_url": { "type": "string", "format": "uri" },
          "first_archived_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this tweet was first archived by us -- not the tweet's own X creation time."
          },
          "removed_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When this tweet was last found removed from X. Null while live, or if removed before this field existed."
          },
          "removed_snapshot": {
            "description": "The archived version that was live right before removal, if known. Null while live.",
            "oneOf": [{ "type": "null" }, { "$ref": "#/components/schemas/SnapshotRef" }]
          },
          "snapshots": {
            "type": "array",
            "description": "Newest first. Empty right after submitting; archiving happens in the background.",
            "items": { "$ref": "#/components/schemas/Snapshot" }
          }
        }
      },
      "SnapshotRef": {
        "type": "object",
        "required": ["wayback_url", "archived_at"],
        "properties": {
          "wayback_url": { "type": "string", "format": "uri", "description": "Permanent archive.org link." },
          "archived_at": { "type": "string", "format": "date-time" }
        }
      },
      "Snapshot": {
        "type": "object",
        "required": ["wayback_url", "archived_at", "screenshot_url", "event"],
        "properties": {
          "wayback_url": { "type": "string", "format": "uri", "description": "Permanent archive.org link." },
          "archived_at": { "type": "string", "format": "date-time" },
          "screenshot_url": { "type": ["string", "null"], "format": "uri" },
          "event": {
            "type": "string",
            "enum": ["created", "edited"],
            "description": "\"created\" for the tweet's first archived version, \"edited\" for every later distinct-content one."
          }
        }
      },
      "ArchiveError": {
        "type": "object",
        "description": "One failed item in a batch, doesn't fail the rest.",
        "required": ["input", "error"],
        "properties": {
          "input": { "type": "string" },
          "error": { "type": "string" }
        }
      },
      "ErrorMessage": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      },
      "ThreadCheckRequest": {
        "type": "object",
        "properties": {
          "x_bearer_token": {
            "type": "string",
            "description": "Your own X API Bearer Token. Without it, only free ancestor context is returned. With it, pays for one X API search call to preview the forward thread."
          }
        }
      },
      "ThreadCheckResponse": {
        "type": "object",
        "required": ["id", "ancestors", "is_reply", "heuristic", "forward"],
        "properties": {
          "id": { "type": "string" },
          "ancestors": {
            "type": "array",
            "description": "What this tweet is replying to, oldest first. Always free, always populated.",
            "items": { "$ref": "#/components/schemas/ArchivedTweet" }
          },
          "is_reply": { "type": "boolean" },
          "heuristic": {
            "type": "object",
            "description": "A free, no-key hint -- never gates spend.",
            "required": ["conversation_count", "looks_like_thread"],
            "properties": {
              "conversation_count": { "type": "integer" },
              "looks_like_thread": { "type": "boolean" }
            }
          },
          "forward": {
            "description": "Null-ish note if no x_bearer_token was given or the budget/search failed; otherwise the forward-thread preview.",
            "type": "object"
          }
        }
      },
      "ThreadIngestRequest": {
        "type": "object",
        "properties": {
          "x_bearer_token": { "type": "string", "description": "Your own X API Bearer Token. Without it, only ancestors are archived." },
          "upgrade_truncated": {
            "type": "boolean",
            "description": "Default true. If false, forward tweets are still archived but truncated ones aren't upgraded to full text."
          },
          "max_cost_usd": {
            "type": "number",
            "description": "If the preview's estimated full-text upgrade cost exceeds this, the upgrade step is skipped (the tweets are still archived, just not upgraded)."
          }
        }
      },
      "ThreadIngestResponse": {
        "type": "object",
        "required": ["id", "ancestors", "forward", "skipped_forward_reason"],
        "properties": {
          "id": { "type": "string" },
          "ancestors": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ArchivedTweet" }
          },
          "forward": {
            "type": ["object", "null"],
            "properties": {
              "tweets": { "type": "array", "items": { "$ref": "#/components/schemas/ArchivedTweet" } },
              "upgraded_count": { "type": "integer" },
              "conversation_id": { "type": "string" }
            }
          },
          "skipped_forward_reason": {
            "type": ["string", "null"],
            "enum": ["no_token", "budget_exhausted", null],
            "description": "Why forward archiving entirely was skipped -- null if it ran (whether or not any tweets were upgraded)."
          }
        }
      }
    }
  }
}
