{
  "openapi": "3.1.0",
  "info": {
    "title": "Floppy API",
    "version": "1.0.0",
    "summary": "Share a file with a short link and see who opened it.",
    "description": "Floppy (https://fpy.sh) shares a file with a short link and shows who opened it: which pages each person read and for how long, with bots filtered out. Agents can use it with no account.\n\n**One call.** `curl -T deck.pdf https://fpy.sh/api/drop/deck.pdf` uploads a file without credentials. The response has `share_url` (the link to send), `agent_key` (shown once: send it as `Authorization: Bearer <agent_key>` on later calls so uploads stay in one account) and `claim_url` (for your human).\n\n**Claiming.** Uploads without an account live in an unclaimed account: 1 GB, safe file types only, up to 20 links, deleted after 72 hours. Give your human the `claim_url`; they sign in with Google and the links, files and view history become theirs. Your agent key keeps working, limited to the links it created.\n\n**`next`.** Every agent-facing response, including errors, has a `next` string: one plain sentence telling you what to do or tell your human.\n\n**Errors** look like `{ \"error\": { \"code\", \"message\", \"next\", \"docs\" } }`. On 402 `limit_reached`, give your human `claim_url` (unclaimed) or `upgrade_url` (claimed). On 429 `slow_down`, wait `retry_after` seconds.\n\n**Auth.** `Authorization: Bearer fpy_live_…` with an agent key or an API key from https://fpy.sh/app/keys. Keys have access `read`, `write` or `read_write` (`x-access` on each operation says what's needed) and scope `account` (everything) or `own` (only the links the key created). See https://fpy.sh/auth.md.\n\n**MCP.** The same features are tools at https://fpy.sh/mcp; `x-mcp-tool` on an operation names its tool.\n\nFull guide: https://fpy.sh/docs/agents",
    "contact": {
      "name": "Lamine Cheloufi",
      "email": "lamine@cheloufi.de",
      "url": "https://fpy.sh"
    }
  },
  "externalDocs": {
    "description": "Floppy for agents",
    "url": "https://fpy.sh/docs/agents"
  },
  "servers": [
    {
      "url": "https://fpy.sh"
    }
  ],
  "security": [
    {
      "bearer": []
    },
    {
      "session": []
    }
  ],
  "tags": [
    {
      "name": "Upload",
      "description": "Whole files in one request (up to 100 MB)."
    },
    {
      "name": "Multipart",
      "description": "Bigger files, or several in one link."
    },
    {
      "name": "Links"
    },
    {
      "name": "Shares",
      "description": "Per-recipient URLs for a link."
    },
    {
      "name": "Analytics",
      "description": "Who read what, page by page. Bots are filtered."
    },
    {
      "name": "Account",
      "description": "Usage, upgrading and claiming."
    },
    {
      "name": "Keys",
      "description": "API keys. Dashboard (session) only."
    }
  ],
  "paths": {
    "/api/drop/{filename}": {
      "put": {
        "operationId": "dropFile",
        "tags": [
          "Upload"
        ],
        "summary": "Upload a file in one call (no account needed)",
        "description": "The one-call drop: `curl -T deck.pdf https://fpy.sh/api/drop/deck.pdf`. Without credentials it creates an unclaimed agent account (1 GB, safe file types only, up to 20 links, deleted after 72 hours unless a human claims it) and returns its `agent_key` once plus a `claim_url` for your human. With `Authorization: Bearer <key>` the file lands in that key's account. A bad key is refused (401) instead of creating a new account. The body is the raw file, up to 100 MB, with a `Content-Length`; use the multipart flow for bigger files.",
        "x-access": "none or write",
        "x-mcp-tool": "upload_file",
        "security": [
          {},
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "description": "The name people see, URL-encoded.",
            "schema": {
              "type": "string"
            },
            "example": "deck.pdf"
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "description": "Also create a per-recipient share with this label (for example `Sequoia – Jane`); `share_url` is then that share's URL.",
            "schema": {
              "type": "string",
              "maxLength": 80
            }
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000000
            }
          },
          {
            "name": "expiry",
            "in": "query",
            "required": false,
            "description": "How long the link works. Unclaimed agent accounts: 3 days at most.",
            "schema": {
              "type": "string",
              "enum": [
                "1D",
                "2D",
                "3D",
                "5D",
                "1W",
                "2W",
                "1M",
                "3M",
                "NEVER"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The file's bytes. `Content-Type` becomes the file's type (guessed from the name when missing or `application/octet-stream`).",
          "content": {
            "*/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored. The scan starts at once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DropResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The Authorization header holds a key that isn't valid (revoked or mistyped).",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "unauthorized",
                    "message": "That API key isn't valid (revoked or mistyped).",
                    "next": "Drop the Authorization header to upload without an account, or create a key at https://fpy.sh/app/keys.",
                    "docs": "https://fpy.sh/docs/agents#errors"
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "description": "A read-only key, or a suspended account (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "411": {
            "description": "No body or no `Content-Length` (`bad_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Send the file as the request body with a Content-Length.",
                    "next": "Use: curl -T deck.pdf https://fpy.sh/api/drop/deck.pdf",
                    "docs": "https://fpy.sh/docs/agents#errors"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "description": "Too many uploads (`retry_after: 60`), or too many new agent accounts from this network today (`retry_after: 3600`; reuse your `agent_key`).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "dropFilePost",
        "tags": [
          "Upload"
        ],
        "summary": "Upload a file in one call (POST form)",
        "description": "Same as `PUT /api/drop/{filename}`, for clients that can only POST. Without credentials it creates an unclaimed agent account (1 GB, safe file types only, up to 20 links, deleted after 72 hours unless a human claims it) and returns its `agent_key` once plus a `claim_url` for your human. With `Authorization: Bearer <key>` the file lands in that key's account. A bad key is refused (401) instead of creating a new account. The body is the raw file, up to 100 MB, with a `Content-Length`; use the multipart flow for bigger files.",
        "x-access": "none or write",
        "x-mcp-tool": "upload_file",
        "security": [
          {},
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "description": "The name people see, URL-encoded.",
            "schema": {
              "type": "string"
            },
            "example": "deck.pdf"
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "description": "Also create a per-recipient share with this label (for example `Sequoia – Jane`); `share_url` is then that share's URL.",
            "schema": {
              "type": "string",
              "maxLength": 80
            }
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000000
            }
          },
          {
            "name": "expiry",
            "in": "query",
            "required": false,
            "description": "How long the link works. Unclaimed agent accounts: 3 days at most.",
            "schema": {
              "type": "string",
              "enum": [
                "1D",
                "2D",
                "3D",
                "5D",
                "1W",
                "2W",
                "1M",
                "3M",
                "NEVER"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The file's bytes. `Content-Type` becomes the file's type (guessed from the name when missing or `application/octet-stream`).",
          "content": {
            "*/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored. The scan starts at once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DropResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The Authorization header holds a key that isn't valid (revoked or mistyped).",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "unauthorized",
                    "message": "That API key isn't valid (revoked or mistyped).",
                    "next": "Drop the Authorization header to upload without an account, or create a key at https://fpy.sh/app/keys.",
                    "docs": "https://fpy.sh/docs/agents#errors"
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "description": "A read-only key, or a suspended account (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "411": {
            "description": "No body or no `Content-Length` (`bad_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "Send the file as the request body with a Content-Length.",
                    "next": "Use: curl -T deck.pdf https://fpy.sh/api/drop/deck.pdf",
                    "docs": "https://fpy.sh/docs/agents#errors"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "description": "Too many uploads (`retry_after: 60`), or too many new agent accounts from this network today (`retry_after: 3600`; reuse your `agent_key`).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/links": {
      "get": {
        "operationId": "listLinks",
        "tags": [
          "Links"
        ],
        "summary": "List links, newest first",
        "description": "An `own`-scope key lists only the links it created.",
        "x-access": "read",
        "x-mcp-tool": "list_links",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Filter by name (substring).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1–200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createLink",
        "tags": [
          "Multipart"
        ],
        "summary": "Create a link and get a multipart upload plan",
        "description": "For files over 100 MB, or several files in one link. Then `PUT` each part to `/api/v1/files/{fileId}/parts/{n}` and `POST /api/v1/files/{fileId}/complete` with the etags.",
        "x-access": "write",
        "x-mcp-tool": "upload_file",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "files"
                ],
                "properties": {
                  "files": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 500,
                    "items": {
                      "$ref": "#/components/schemas/NewFile"
                    }
                  },
                  "folderId": {
                    "type": "string",
                    "description": "Put the link in this folder (ignored if it isn't yours)."
                  }
                }
              },
              "example": {
                "files": [
                  {
                    "name": "big.mov",
                    "size": 50000000,
                    "type": "video/quicktime"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link (status `uploading`) and one upload per file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadPlan"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "description": "A file over 50 GB (`limit_reached`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/links/from-url": {
      "post": {
        "operationId": "createLinkFromUrl",
        "tags": [
          "Upload"
        ],
        "summary": "Upload a file from a URL",
        "description": "Floppy fetches the URL server-side (public http(s) only; private, metadata and fpy.sh addresses are refused; up to 4 redirects; up to 100 MB).",
        "x-access": "write",
        "x-mcp-tool": "upload_file",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "name": {
                    "type": "string",
                    "description": "Defaults to the response's filename or the URL's last path segment."
                  },
                  "label": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Also create a per-recipient share with this label."
                  }
                }
              },
              "example": {
                "url": "https://example.com/deck.pdf",
                "label": "Sequoia – Jane"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored, same shape as the drop (without `limits`, `claim_url` or `agent_key`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DropResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      }
    },
    "/api/v1/links/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "get": {
        "operationId": "getLink",
        "tags": [
          "Links"
        ],
        "summary": "Get a link with its files and scan state",
        "description": "Poll this to see the scan verdict. Ready files carry a signed `download_url` (10 minutes).",
        "x-access": "read",
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateLink",
        "tags": [
          "Links"
        ],
        "summary": "Change a link's settings",
        "description": "Only the fields sent change. Unknown `expiry` values are ignored.",
        "x-access": "write",
        "x-mcp-tool": "update_link",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expiry": {
                    "$ref": "#/components/schemas/Expiry"
                  },
                  "password": {
                    "type": "string",
                    "description": "Set a password; an empty string removes it."
                  },
                  "downloads_allowed": {
                    "type": "boolean"
                  },
                  "downloadsAllowed": {
                    "type": "boolean",
                    "description": "camelCase alias of `downloads_allowed`."
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "An empty string clears it."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              },
              "example": {
                "name": "Series A deck",
                "expiry": "1W"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "revokeLink",
        "tags": [
          "Links"
        ],
        "summary": "Revoke a link and all its shares",
        "x-access": "write",
        "x-mcp-tool": "revoke_share",
        "responses": {
          "200": {
            "description": "Revoked. Files are deleted within a week.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true,
                  "next": "https://fpy.sh/k3x9 and all its shares no longer open. Files are deleted within a week."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/files/{id}/parts/{n}": {
      "put": {
        "operationId": "uploadPart",
        "tags": [
          "Multipart"
        ],
        "summary": "Upload one part of a file",
        "description": "Parts are 1-based and `partSize` (32 MiB) each, except the last. Keep each `etag` for the complete call. Errors here use the plain `{ error }` shape. Also served at `/api/files/{id}/parts/{n}`.",
        "x-access": "write",
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "n",
            "in": "path",
            "required": true,
            "description": "Part number, 1-based.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            }
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 33554432
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Part stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No upload in progress for this file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                },
                "example": {
                  "error": "No upload in progress"
                }
              }
            }
          },
          "411": {
            "description": "Missing `Content-Length`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                },
                "example": {
                  "error": "Part needs a Content-Length"
                }
              }
            }
          },
          "413": {
            "description": "Part over 32 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                },
                "example": {
                  "error": "Part too large"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/files/{id}/complete": {
      "post": {
        "operationId": "completeFile",
        "tags": [
          "Multipart"
        ],
        "summary": "Finish a file's multipart upload",
        "description": "The file is then scanned before anyone else can open it. Errors here use the plain `{ error }` shape. Also served at `/api/files/{id}/complete`.",
        "x-access": "write",
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "parts"
                ],
                "properties": {
                  "parts": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "partNumber",
                        "etag"
                      ],
                      "properties": {
                        "partNumber": {
                          "type": "integer"
                        },
                        "etag": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "parts": [
                  {
                    "partNumber": 1,
                    "etag": "\"7f2c…\""
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteResult"
                }
              }
            }
          },
          "400": {
            "description": "No parts sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                },
                "example": {
                  "error": "Missing parts"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No upload in progress for this file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                },
                "example": {
                  "error": "No upload in progress"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/links/{id}/shares": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "get": {
        "operationId": "listShares",
        "tags": [
          "Shares"
        ],
        "summary": "List a link's shares",
        "description": "The link's own URL comes first as the `General` share (`id: null`).",
        "x-access": "read",
        "responses": {
          "200": {
            "description": "The shares.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "createShare",
        "tags": [
          "Shares"
        ],
        "summary": "Make a separate URL for one recipient",
        "description": "Each recipient gets their own URL, so you see what each person read without an email gate. Up to 200 shares per link.",
        "x-access": "write",
        "x-mcp-tool": "create_share",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "expiry": {
                    "$ref": "#/components/schemas/Expiry"
                  }
                }
              },
              "example": {
                "label": "Sequoia – Jane"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new share.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/LimitReached"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/shares/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShareId"
        }
      ],
      "patch": {
        "operationId": "updateShare",
        "tags": [
          "Shares"
        ],
        "summary": "Rename or turn a share on or off",
        "x-access": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "active": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated share.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Share"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "revokeShare",
        "tags": [
          "Shares"
        ],
        "summary": "Turn off one recipient's URL",
        "description": "Other shares of the link keep working.",
        "x-access": "write",
        "x-mcp-tool": "revoke_share",
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true,
                  "next": "https://fpy.sh/m2p8 no longer opens. Other shares of this link still work."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{id}/analytics": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "get": {
        "operationId": "getLinkAnalytics",
        "tags": [
          "Analytics"
        ],
        "summary": "Who read a link, page by page",
        "description": "People, pages reached, time per page and completion. Bots, link previews and visits with no real input are filtered out and counted in `filtered`.",
        "x-access": "read",
        "x-mcp-tool": "get_link_analytics",
        "responses": {
          "200": {
            "description": "The analytics.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkAnalytics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/shares/{id}/timeline": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShareId"
        }
      ],
      "get": {
        "operationId": "getShareTimeline",
        "tags": [
          "Analytics"
        ],
        "summary": "One recipient's visits, page by page",
        "x-access": "read",
        "x-mcp-tool": "get_viewer_timeline",
        "responses": {
          "200": {
            "description": "The visits, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeline"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{id}/timeline": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "get": {
        "operationId": "getLinkTimeline",
        "tags": [
          "Analytics"
        ],
        "summary": "Visits through the link's own (general) URL",
        "description": "Like the share timeline, for the link's own URL (`share.id` is null).",
        "x-access": "read",
        "responses": {
          "200": {
            "description": "The visits, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeline"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/activity": {
      "get": {
        "operationId": "listActivity",
        "tags": [
          "Analytics"
        ],
        "summary": "Real visits across your links since a time",
        "description": "Newest first, up to 200.",
        "x-access": "read",
        "x-mcp-tool": "who_viewed_since",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "An ISO time, or a duration like `30m`, `24h` or `7d`. Defaults to 24 hours ago.",
            "schema": {
              "type": "string"
            },
            "examples": {
              "duration": {
                "value": "24h"
              },
              "iso": {
                "value": "2026-11-06T00:00:00Z"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The visits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Activity"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Account"
        ],
        "summary": "Plan, storage used and limits",
        "description": "An agent can poll this to see when its human has claimed the account (`claimed` turns true).",
        "x-access": "read",
        "x-mcp-tool": "get_usage",
        "responses": {
          "200": {
            "description": "The usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/upgrade": {
      "post": {
        "operationId": "getUpgradeLink",
        "tags": [
          "Account"
        ],
        "summary": "A link for your human to upgrade",
        "description": "Claimed accounts get `upgrade_url` (it signs the human in and opens Stripe Checkout; Pro accounts get the plan page). Unclaimed accounts can't pay, so they get `claim_url`: the claim page offers \"Claim and upgrade\". Changes nothing, so read-only keys may call it.",
        "x-access": "read",
        "x-mcp-tool": "get_upgrade_link",
        "responses": {
          "200": {
            "description": "Where to send the human.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpgradeLink"
                },
                "examples": {
                  "claimed": {
                    "value": {
                      "upgrade_url": "https://fpy.sh/app/plan?upgrade=1",
                      "next": "Give your human this link: https://fpy.sh/app/plan?upgrade=1. It signs them in and opens Stripe Checkout for Pro (€8/mo); the same key gets Pro limits as soon as they pay."
                    }
                  },
                  "unclaimed": {
                    "value": {
                      "claim_url": "https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d",
                      "next": "Ask your human to open https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d and choose \"Claim and upgrade to Pro\". Unclaimed accounts can't pay until a person claims them."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/claim": {
      "get": {
        "operationId": "getClaimLink",
        "tags": [
          "Account"
        ],
        "summary": "A fresh claim link for an unclaimed account",
        "description": "Lost the `claim_url`? Call this with your agent key. Each call mints a new link; all stay valid until the account is claimed or expires.",
        "x-access": "read",
        "x-mcp-tool": "get_claim_link",
        "responses": {
          "200": {
            "description": "The claim link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClaimLink"
                }
              }
            }
          },
          "400": {
            "description": "The account is already claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_request",
                    "message": "This account is already claimed.",
                    "next": "Nothing to do — your human owns these files.",
                    "docs": "https://fpy.sh/docs/agents#errors"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/tokens": {
      "get": {
        "operationId": "listTokens",
        "tags": [
          "Keys"
        ],
        "summary": "List API keys (dashboard only)",
        "description": "Browser sessions only: a key can never list or mint keys (403).",
        "x-session-only": true,
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Live keys, newest first, without secrets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tokens"
                  ],
                  "properties": {
                    "tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Token"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createToken",
        "tags": [
          "Keys"
        ],
        "summary": "Create an API key (dashboard only)",
        "description": "The secret is returned once, here.",
        "x-session-only": true,
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "access"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 60
                  },
                  "access": {
                    "$ref": "#/components/schemas/TokenAccess"
                  },
                  "scope": {
                    "$ref": "#/components/schemas/TokenScope",
                    "default": "account"
                  }
                }
              },
              "example": {
                "name": "Claude",
                "access": "read_write"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The key and its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "token",
                    "secret"
                  ],
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/Token"
                    },
                    "secret": {
                      "type": "string",
                      "pattern": "^fpy_live_[0-9a-f]{48}$"
                    }
                  }
                },
                "example": {
                  "token": {
                    "id": "9e8d7c6b5a4f3e2d1c0b",
                    "user_id": "1a2b3c4d5e6f708192a3b4c5",
                    "name": "Claude",
                    "prefix": "fpy_live_0f1e",
                    "access": "read_write",
                    "scope": "account",
                    "created_at": 1762430580000,
                    "last_used_at": null,
                    "revoked_at": null
                  },
                  "secret": "fpy_live_0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a6978"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/tokens/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/TokenId"
        }
      ],
      "delete": {
        "operationId": "revokeToken",
        "tags": [
          "Keys"
        ],
        "summary": "Revoke an API key (dashboard only)",
        "description": "Takes effect on the next request.",
        "x-session-only": true,
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "widenToken",
        "tags": [
          "Keys"
        ],
        "summary": "Widen an `own` key to the whole account (dashboard only)",
        "description": "One-way: a key can be widened to `account`, never narrowed. To narrow, revoke it and create a new key.",
        "x-session-only": true,
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope"
                ],
                "properties": {
                  "scope": {
                    "type": "string",
                    "const": "account"
                  }
                }
              },
              "example": {
                "scope": "account"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "token"
                  ],
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/Token"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fpy_live_…",
        "description": "An agent key (from a drop without credentials) or an API key from https://fpy.sh/app/keys, sent as `Authorization: Bearer fpy_live_…`. Tokens start with `fpy_live_`. A bad Bearer never falls back to the cookie. Bearer requests skip the same-origin check."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "fpy_s",
        "description": "The dashboard's Google sign-in session. For the browser only: cookie writes must be same-origin. Agents use `bearer`."
      }
    },
    "parameters": {
      "LinkId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Link id.",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]+$"
        }
      },
      "ShareId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Share id (not the slug).",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]+$"
        }
      },
      "FileId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "File id from the upload plan (`uploads[].fileId`).",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]+$"
        }
      },
      "TokenId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "API key id.",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]+$"
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "description": "Machine-readable error code. `billing_unavailable` is only sent by the dashboard's checkout and billing-portal endpoints. `file_blocked` is reserved: blocked files are reported through `scan: \"blocked\"` today.",
        "enum": [
          "unauthorized",
          "forbidden",
          "not_found",
          "bad_request",
          "limit_reached",
          "slow_down",
          "file_blocked",
          "conflict",
          "billing_unavailable"
        ]
      },
      "Error": {
        "type": "object",
        "description": "The error shape of the agent API. `next` says what to do, `docs` links to the error table.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "docs"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string",
                "description": "What went wrong, in one sentence."
              },
              "next": {
                "type": "string",
                "description": "What to do or tell your human next."
              },
              "upgrade_url": {
                "type": "string",
                "format": "uri",
                "description": "402 on a claimed account: where your human upgrades to Pro."
              },
              "claim_url": {
                "type": "string",
                "format": "uri",
                "description": "402 on an unclaimed account: where your human claims it, which lifts the agent limits."
              },
              "retry_after": {
                "type": "integer",
                "description": "429: seconds to wait before retrying (also in the `Retry-After` header)."
              },
              "reason": {
                "type": "string",
                "description": "Why a file was blocked, for example `malware` or `type_not_allowed`."
              },
              "docs": {
                "type": "string",
                "format": "uri",
                "const": "https://fpy.sh/docs/agents#errors"
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "limit_reached",
            "message": "That's more than the 1 GB this plan includes (0 MB used).",
            "claim_url": "https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d",
            "next": "Ask your human to claim this account at https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d — claiming lifts the agent limits and lets them upgrade.",
            "docs": "https://fpy.sh/docs/agents#errors"
          }
        }
      },
      "PlainError": {
        "type": "object",
        "description": "The older dashboard error shape, still returned by the router's sign-in and access checks (401/403 before a handler runs), by unknown /api paths, and by the multipart part/complete endpoints.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "example": {
          "error": "Sign in required"
        }
      },
      "Plan": {
        "type": "string",
        "description": "`unclaimed`: an agent account nobody has claimed yet (1 GB, deleted after 72 hours). `beta`: early accounts, treated like Pro.",
        "enum": [
          "unclaimed",
          "free",
          "pro",
          "beta"
        ]
      },
      "ScanState": {
        "type": "string",
        "description": "Every file is scanned before anyone but its owner can open it. Blocked files are never served.",
        "enum": [
          "uploading",
          "scanning",
          "clean",
          "screened",
          "blocked",
          "unscanned-large"
        ]
      },
      "FileKind": {
        "type": "string",
        "enum": [
          "video",
          "zip",
          "image",
          "pdf",
          "audio",
          "doc"
        ]
      },
      "Expiry": {
        "type": "string",
        "description": "How long a link (or share) stays open. Capped by the plan's `maxExpiryDays` (402 over it).",
        "enum": [
          "1D",
          "2D",
          "3D",
          "5D",
          "1W",
          "2W",
          "1M",
          "3M",
          "NEVER"
        ]
      },
      "Limits": {
        "type": "object",
        "description": "Plan limits.",
        "required": [
          "storageBytes",
          "maxFileBytes",
          "maxExpiryDays",
          "maxLinks"
        ],
        "properties": {
          "storageBytes": {
            "type": "integer"
          },
          "maxFileBytes": {
            "type": "integer"
          },
          "maxExpiryDays": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Longest expiry in days; null means `NEVER` is allowed."
          },
          "maxLinks": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Live links at a time; null means no limit."
          }
        },
        "example": {
          "storageBytes": 1073741824,
          "maxFileBytes": 1073741824,
          "maxExpiryDays": 3,
          "maxLinks": 20
        }
      },
      "DropLimits": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Limits"
          },
          {
            "type": "object",
            "properties": {
              "expires_in_hours": {
                "type": "integer",
                "description": "Unclaimed accounts only: hours until the account and its files are deleted.",
                "example": 72
              }
            }
          }
        ]
      },
      "LinkSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "url",
          "slug",
          "kind",
          "size",
          "status",
          "scan",
          "created_at",
          "expires_at",
          "shares",
          "views",
          "via_agent"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-f0-9]+$"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The link's own (general) URL."
          },
          "slug": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/FileKind"
          },
          "size": {
            "type": "integer",
            "description": "Total bytes."
          },
          "status": {
            "type": "string",
            "enum": [
              "uploading",
              "ready",
              "expired",
              "revoked"
            ]
          },
          "scan": {
            "$ref": "#/components/schemas/ScanState"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "shares": {
            "type": "integer",
            "description": "Per-recipient shares (not counting the general URL)."
          },
          "views": {
            "type": "integer",
            "description": "Real visits: bots and no-input visits are not counted."
          },
          "via_agent": {
            "type": "boolean",
            "description": "Created with an API or agent key."
          }
        },
        "example": {
          "id": "5c1e9a7b3d2f4e6a8b0c1d2e",
          "name": "deck.pdf",
          "url": "https://fpy.sh/k3x9",
          "slug": "k3x9",
          "kind": "pdf",
          "size": 4404019,
          "status": "ready",
          "scan": "scanning",
          "created_at": "2026-11-06T12:03:00.000Z",
          "expires_at": "2026-11-09T12:03:00.000Z",
          "shares": 0,
          "views": 0,
          "via_agent": true
        }
      },
      "LinkFile": {
        "type": "object",
        "required": [
          "id",
          "path",
          "size",
          "content_type",
          "scan",
          "download_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-f0-9]+$"
          },
          "path": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "scan": {
            "$ref": "#/components/schemas/ScanState"
          },
          "download_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Signed download URL, valid for 10 minutes. Null until the file is ready (scanned clean)."
          }
        }
      },
      "LinkDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LinkSummary"
          },
          {
            "type": "object",
            "required": [
              "files",
              "password",
              "email_gate",
              "downloads_allowed",
              "note",
              "pages",
              "limits",
              "next"
            ],
            "properties": {
              "files": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LinkFile"
                }
              },
              "password": {
                "type": "boolean",
                "description": "Whether a password is set."
              },
              "email_gate": {
                "type": "boolean"
              },
              "downloads_allowed": {
                "type": "boolean"
              },
              "note": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "pages": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Page count reported by viewers, if any."
              },
              "limits": {
                "$ref": "#/components/schemas/Limits"
              },
              "next": {
                "type": "string"
              }
            }
          }
        ],
        "example": {
          "id": "5c1e9a7b3d2f4e6a8b0c1d2e",
          "name": "deck.pdf",
          "url": "https://fpy.sh/k3x9",
          "slug": "k3x9",
          "kind": "pdf",
          "size": 4404019,
          "status": "ready",
          "scan": "clean",
          "created_at": "2026-11-06T12:03:00.000Z",
          "expires_at": "2026-11-09T12:03:00.000Z",
          "shares": 1,
          "views": 1,
          "via_agent": true,
          "files": [
            {
              "id": "a1b2c3d4e5f60718293a4b5c",
              "path": "deck.pdf",
              "size": 4404019,
              "content_type": "application/pdf",
              "scan": "clean",
              "download_url": "https://fpy.sh/f/a1b2c3d4e5f60718293a4b5c?dl=1&exp=1762431180&sig=3b1f0c…"
            }
          ],
          "password": false,
          "email_gate": false,
          "downloads_allowed": true,
          "note": null,
          "pages": 14,
          "limits": {
            "storageBytes": 1073741824,
            "maxFileBytes": 1073741824,
            "maxExpiryDays": 3,
            "maxLinks": 20
          },
          "next": "Share url with one person, or make one link per recipient with create_share / POST /api/v1/links/:id/shares."
        }
      },
      "Share": {
        "type": "object",
        "description": "A per-recipient URL for a link. The link's own URL is listed as the `General` share, with `id: null`.",
        "required": [
          "id",
          "link_id",
          "slug",
          "url",
          "label",
          "active",
          "created_at",
          "opened_at",
          "reading_now"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null for the link's own (general) URL."
          },
          "link_id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "label": {
            "type": "string",
            "description": "Who it's for, for example `Sequoia – Jane`."
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "opened_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "First view through this URL."
          },
          "reading_now": {
            "type": "boolean",
            "description": "A real visitor was active on it in the last 30 seconds."
          }
        },
        "example": {
          "id": "7d3e9f1a2b4c6d8e0f1a",
          "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
          "slug": "m2p8",
          "url": "https://fpy.sh/m2p8",
          "label": "Sequoia – Jane",
          "active": true,
          "created_at": "2026-11-06T12:03:00.000Z",
          "opened_at": null,
          "reading_now": false
        }
      },
      "ShareCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Share"
          },
          {
            "type": "object",
            "required": [
              "next"
            ],
            "properties": {
              "next": {
                "type": "string"
              }
            }
          }
        ],
        "example": {
          "id": "7d3e9f1a2b4c6d8e0f1a",
          "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
          "slug": "m2p8",
          "url": "https://fpy.sh/m2p8",
          "label": "Sequoia – Jane",
          "active": true,
          "created_at": "2026-11-06T12:03:00.000Z",
          "opened_at": null,
          "reading_now": false,
          "next": "Send https://fpy.sh/m2p8 to Sequoia – Jane only. You'll see exactly what they read."
        }
      },
      "ShareList": {
        "type": "object",
        "required": [
          "shares",
          "next"
        ],
        "properties": {
          "shares": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Share"
            }
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "shares": [
            {
              "id": null,
              "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
              "slug": "k3x9",
              "url": "https://fpy.sh/k3x9",
              "label": "General",
              "active": true,
              "created_at": "2026-11-06T12:03:00.000Z",
              "opened_at": "2026-11-06T14:20:11.000Z",
              "reading_now": false
            },
            {
              "id": "7d3e9f1a2b4c6d8e0f1a",
              "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
              "slug": "m2p8",
              "url": "https://fpy.sh/m2p8",
              "label": "Sequoia – Jane",
              "active": true,
              "created_at": "2026-11-06T12:03:00.000Z",
              "opened_at": null,
              "reading_now": false
            }
          ],
          "next": "Each share is a separate URL for one recipient. Create one per person with POST /api/v1/links/:id/shares."
        }
      },
      "ViewerSummary": {
        "type": "object",
        "required": [
          "share_id",
          "label",
          "visits",
          "pages_reached",
          "pages_total",
          "active_ms",
          "completion",
          "last_seen",
          "device",
          "city",
          "country"
        ],
        "properties": {
          "share_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null for visits through the general URL."
          },
          "label": {
            "type": "string"
          },
          "visits": {
            "type": "integer"
          },
          "pages_reached": {
            "type": "integer",
            "description": "Pages with at least one second of reading."
          },
          "pages_total": {
            "type": [
              "integer",
              "null"
            ]
          },
          "active_ms": {
            "type": "integer",
            "description": "Visible, focused time across visits."
          },
          "completion": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "last_seen": {
            "type": "string",
            "format": "date-time"
          },
          "device": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PageStat": {
        "type": "object",
        "required": [
          "page",
          "avg_ms",
          "reached"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "avg_ms": {
            "type": "integer",
            "description": "Average time among people who spent at least a second there."
          },
          "reached": {
            "type": "integer",
            "description": "Recipients who read this page."
          }
        }
      },
      "LinkAnalytics": {
        "type": "object",
        "required": [
          "link_id",
          "views",
          "people",
          "pages",
          "filtered",
          "next"
        ],
        "properties": {
          "link_id": {
            "type": "string"
          },
          "views": {
            "type": "integer",
            "description": "Real visits."
          },
          "people": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ViewerSummary"
            },
            "description": "One entry per recipient (share), most recent first."
          },
          "pages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageStat"
            }
          },
          "filtered": {
            "type": "object",
            "required": [
              "count",
              "reasons"
            ],
            "description": "Visits kept apart and never counted: bots, link previews, datacenter traffic and visits with no real input.",
            "properties": {
              "count": {
                "type": "integer"
              },
              "reasons": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
          "views": 1,
          "people": [
            {
              "share_id": "7d3e9f1a2b4c6d8e0f1a",
              "label": "Sequoia – Jane",
              "visits": 1,
              "pages_reached": 3,
              "pages_total": 14,
              "active_ms": 39000,
              "completion": 0.21428571428571427,
              "last_seen": "2026-11-06T14:24:40.000Z",
              "device": "Desktop",
              "city": "San Francisco",
              "country": "US"
            }
          ],
          "pages": [
            {
              "page": 1,
              "avg_ms": 5000,
              "reached": 1
            },
            {
              "page": 2,
              "avg_ms": 4000,
              "reached": 1
            },
            {
              "page": 9,
              "avg_ms": 30000,
              "reached": 1
            }
          ],
          "filtered": {
            "count": 3,
            "reasons": {
              "bot": 1,
              "link_preview": 1,
              "no_input": 1
            }
          },
          "next": "1 person read it. Call get_viewer_timeline for page-by-page detail."
        }
      },
      "Visit": {
        "type": "object",
        "required": [
          "session_id",
          "started_at",
          "active_ms",
          "device",
          "browser",
          "city",
          "pages",
          "summary"
        ],
        "properties": {
          "session_id": {
            "type": "string"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "active_ms": {
            "type": "integer"
          },
          "device": {
            "type": [
              "string",
              "null"
            ]
          },
          "browser": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "pages": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "page",
                "ms"
              ],
              "properties": {
                "page": {
                  "type": "integer"
                },
                "ms": {
                  "type": "integer"
                }
              }
            }
          },
          "summary": {
            "type": "string",
            "description": "One plain sentence written server-side, such as \"Jumped straight to page 9 (2:58 there)\"."
          }
        },
        "example": {
          "session_id": "sessjane00000000",
          "started_at": "2026-11-06T14:20:11.000Z",
          "active_ms": 45000,
          "device": "Desktop",
          "browser": "Safari",
          "city": "San Francisco",
          "pages": [
            {
              "page": 1,
              "ms": 2000
            },
            {
              "page": 9,
              "ms": 45000
            }
          ],
          "summary": "Jumped straight to page 9 (0:45 there)"
        }
      },
      "Timeline": {
        "type": "object",
        "required": [
          "share",
          "visits",
          "next"
        ],
        "properties": {
          "share": {
            "$ref": "#/components/schemas/Share"
          },
          "visits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Visit"
            },
            "description": "Newest first."
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "share": {
            "id": "7d3e9f1a2b4c6d8e0f1a",
            "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
            "slug": "m2p8",
            "url": "https://fpy.sh/m2p8",
            "label": "Sequoia – Jane",
            "active": true,
            "created_at": "2026-11-06T12:03:00.000Z",
            "opened_at": "2026-11-06T14:20:11.000Z",
            "reading_now": false
          },
          "visits": [
            {
              "session_id": "sessjane00000000",
              "started_at": "2026-11-06T14:20:11.000Z",
              "active_ms": 45000,
              "device": "Desktop",
              "browser": "Safari",
              "city": "San Francisco",
              "pages": [
                {
                  "page": 1,
                  "ms": 2000
                },
                {
                  "page": 9,
                  "ms": 45000
                }
              ],
              "summary": "Jumped straight to page 9 (0:45 there)"
            }
          ],
          "next": "Most recent: Jumped straight to page 9 (0:45 there)."
        }
      },
      "ActivityItem": {
        "type": "object",
        "required": [
          "at",
          "link_id",
          "link_name",
          "share_label",
          "action"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "link_id": {
            "type": "string"
          },
          "link_name": {
            "type": "string"
          },
          "share_label": {
            "type": "string",
            "description": "`General` for the link's own URL."
          },
          "action": {
            "type": "string",
            "description": "A plain sentence about the visit."
          }
        }
      },
      "Activity": {
        "type": "object",
        "required": [
          "events",
          "next"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActivityItem"
            },
            "maxItems": 200,
            "description": "Newest first."
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "events": [
            {
              "at": "2026-11-06T14:20:11.000Z",
              "link_id": "5c1e9a7b3d2f4e6a8b0c1d2e",
              "link_name": "deck.pdf",
              "share_label": "Sequoia – Jane",
              "action": "Jumped straight to page 9 (0:45 there)"
            }
          ],
          "next": "1 real visits since 2026-11-05T14:30:00.000Z."
        }
      },
      "LinkList": {
        "type": "object",
        "required": [
          "links",
          "next"
        ],
        "properties": {
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkSummary"
            }
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "links": [
            {
              "id": "5c1e9a7b3d2f4e6a8b0c1d2e",
              "name": "deck.pdf",
              "url": "https://fpy.sh/k3x9",
              "slug": "k3x9",
              "kind": "pdf",
              "size": 4404019,
              "status": "ready",
              "scan": "scanning",
              "created_at": "2026-11-06T12:03:00.000Z",
              "expires_at": "2026-11-09T12:03:00.000Z",
              "shares": 0,
              "views": 0,
              "via_agent": true
            }
          ],
          "next": "Use get_link_analytics / GET /api/v1/links/:id/analytics to see who read each one."
        }
      },
      "DropResult": {
        "type": "object",
        "description": "What you get after uploading a whole file (drop or from-url).",
        "required": [
          "link",
          "share",
          "share_url",
          "analytics_url",
          "scan",
          "next"
        ],
        "properties": {
          "link": {
            "$ref": "#/components/schemas/LinkSummary"
          },
          "share": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ShareCreated"
              },
              {
                "type": "null"
              }
            ],
            "description": "The per-recipient share made from `label`, or null."
          },
          "share_url": {
            "type": "string",
            "format": "uri",
            "description": "The link to send: the share's URL if a label was given, else the link's own URL."
          },
          "analytics_url": {
            "type": "string",
            "format": "uri",
            "description": "GET it with the same key to see who read the file."
          },
          "claim_url": {
            "type": "string",
            "format": "uri",
            "description": "Unclaimed accounts only: give this to your human. They sign in with Google and the files become theirs."
          },
          "agent_key": {
            "type": "string",
            "pattern": "^fpy_live_[0-9a-f]{48}$",
            "description": "Only when this call created a new unclaimed account. Shown once: save it and send it as `Authorization: Bearer <agent_key>` on later calls."
          },
          "scan": {
            "type": "string",
            "description": "The malware check has started. Poll GET /api/v1/links/{id} and read `scan` for the verdict.",
            "$ref": "#/components/schemas/ScanState"
          },
          "limits": {
            "$ref": "#/components/schemas/DropLimits",
            "description": "Drop only (not from-url)."
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "link": {
            "id": "5c1e9a7b3d2f4e6a8b0c1d2e",
            "name": "deck.pdf",
            "url": "https://fpy.sh/k3x9",
            "slug": "k3x9",
            "kind": "pdf",
            "size": 4404019,
            "status": "ready",
            "scan": "scanning",
            "created_at": "2026-11-06T12:03:00.000Z",
            "expires_at": "2026-11-09T12:03:00.000Z",
            "shares": 0,
            "views": 0,
            "via_agent": true
          },
          "share": null,
          "share_url": "https://fpy.sh/k3x9",
          "analytics_url": "https://fpy.sh/api/v1/links/5c1e9a7b3d2f4e6a8b0c1d2e/analytics",
          "claim_url": "https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d",
          "agent_key": "fpy_live_0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a6978",
          "scan": "scanning",
          "limits": {
            "storageBytes": 1073741824,
            "maxFileBytes": 1073741824,
            "maxExpiryDays": 3,
            "maxLinks": 20,
            "expires_in_hours": 72
          },
          "next": "Share https://fpy.sh/k3x9. It opens once the malware check passes (a few seconds). Save agent_key now — it is shown only once. Use it as 'Authorization: Bearer <agent_key>' to add files or read who viewed them. Tell your human: claim these files at https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d within 72 hours, or they're deleted."
        }
      },
      "NewFile": {
        "type": "object",
        "required": [
          "name",
          "size"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "size": {
            "type": "integer",
            "minimum": 0,
            "maximum": 53687091200,
            "description": "Bytes. Up to 50 GB, and the plan's `maxFileBytes`."
          },
          "type": {
            "type": "string",
            "description": "Content type, for example `application/pdf`."
          }
        }
      },
      "UploadPlan": {
        "type": "object",
        "required": [
          "link",
          "uploads",
          "next"
        ],
        "properties": {
          "link": {
            "$ref": "#/components/schemas/LinkDetail"
          },
          "uploads": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "fileId",
                "partSize",
                "parts"
              ],
              "properties": {
                "fileId": {
                  "type": "string"
                },
                "partSize": {
                  "type": "integer",
                  "const": 33554432,
                  "description": "Bytes per part (32 MiB); the last part may be smaller."
                },
                "parts": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "link": {
            "id": "5c1e9a7b3d2f4e6a8b0c1d2e",
            "name": "big.mov",
            "url": "https://fpy.sh/k3x9",
            "slug": "k3x9",
            "kind": "video",
            "size": 50000000,
            "status": "uploading",
            "scan": "uploading",
            "created_at": "2026-11-06T12:03:00.000Z",
            "expires_at": "2026-11-09T12:03:00.000Z",
            "shares": 1,
            "views": 1,
            "via_agent": true,
            "files": [
              {
                "id": "a1b2c3d4e5f60718293a4b5c",
                "path": "big.mov",
                "size": 50000000,
                "content_type": "video/quicktime",
                "scan": "uploading",
                "download_url": null
              }
            ],
            "password": false,
            "email_gate": false,
            "downloads_allowed": true,
            "note": null,
            "pages": 14,
            "limits": {
              "storageBytes": 1073741824,
              "maxFileBytes": 1073741824,
              "maxExpiryDays": 3,
              "maxLinks": 20
            },
            "next": "Share url with one person, or make one link per recipient with create_share / POST /api/v1/links/:id/shares."
          },
          "uploads": [
            {
              "fileId": "a1b2c3d4e5f60718293a4b5c",
              "partSize": 33554432,
              "parts": 2
            }
          ],
          "next": "PUT each part to /api/v1/files/<fileId>/parts/<n> (1-based, 33554432 bytes each), then POST /api/v1/files/<fileId>/complete with the etags."
        }
      },
      "PartResult": {
        "type": "object",
        "required": [
          "partNumber",
          "etag"
        ],
        "properties": {
          "partNumber": {
            "type": "integer"
          },
          "etag": {
            "type": "string"
          }
        },
        "example": {
          "partNumber": 1,
          "etag": "\"7f2c…\""
        }
      },
      "CompleteResult": {
        "type": "object",
        "required": [
          "ok",
          "linkReady"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "linkReady": {
            "type": "boolean",
            "description": "True once every file of the link is uploaded. Files are then scanned before anyone else can open them."
          }
        },
        "example": {
          "ok": true,
          "linkReady": true
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok",
          "next"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "next": {
            "type": "string"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "plan",
          "claimed",
          "used_bytes",
          "limits",
          "link_count",
          "next"
        ],
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "claimed": {
            "type": "boolean",
            "description": "Turns true once your human has claimed the account."
          },
          "used_bytes": {
            "type": "integer"
          },
          "limits": {
            "$ref": "#/components/schemas/Limits"
          },
          "link_count": {
            "type": "integer",
            "description": "Live (uploading or ready) links."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Unclaimed accounts only: when the account and its links are deleted."
          },
          "plan_status": {
            "type": "string",
            "description": "Pro only: the Stripe subscription status."
          },
          "renews_at": {
            "type": "string",
            "format": "date-time",
            "description": "Pro only: when the current period ends."
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "plan": "free",
          "claimed": true,
          "used_bytes": 1500,
          "limits": {
            "storageBytes": 2147483648,
            "maxFileBytes": 524288000,
            "maxExpiryDays": 30,
            "maxLinks": null
          },
          "link_count": 2,
          "next": "0 MB of 2 GB used. For more, your human can upgrade to Pro (€8/mo) at https://fpy.sh/app/plan?upgrade=1."
        }
      },
      "UpgradeLink": {
        "oneOf": [
          {
            "type": "object",
            "title": "Claimed",
            "required": [
              "upgrade_url",
              "next"
            ],
            "properties": {
              "upgrade_url": {
                "type": "string",
                "format": "uri"
              },
              "next": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "title": "Unclaimed",
            "required": [
              "claim_url",
              "next"
            ],
            "properties": {
              "claim_url": {
                "type": "string",
                "format": "uri"
              },
              "next": {
                "type": "string"
              }
            }
          }
        ]
      },
      "ClaimLink": {
        "type": "object",
        "required": [
          "claim_url",
          "expires_at",
          "next"
        ],
        "properties": {
          "claim_url": {
            "type": "string",
            "format": "uri",
            "description": "A fresh claim link each call; earlier ones keep working until the account is claimed or expires."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "next": {
            "type": "string"
          }
        },
        "example": {
          "claim_url": "https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d",
          "expires_at": "2026-11-09T12:03:00.000Z",
          "next": "Send your human https://fpy.sh/claim/7f3c2a9e1b4d6c8f0a2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d. They sign in with Google and the files become theirs."
        }
      },
      "TokenAccess": {
        "type": "string",
        "enum": [
          "read",
          "write",
          "read_write"
        ]
      },
      "TokenScope": {
        "type": "string",
        "description": "`account`: everything in the account. `own`: only the links the key created (the scope of agent keys).",
        "enum": [
          "own",
          "account"
        ]
      },
      "Token": {
        "type": "object",
        "description": "An API key without its secret. Timestamps here are milliseconds since the epoch.",
        "required": [
          "id",
          "user_id",
          "name",
          "prefix",
          "access",
          "scope",
          "created_at",
          "last_used_at",
          "revoked_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "user_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "prefix": {
            "type": "string",
            "description": "The first 13 characters of the secret, for recognizing it."
          },
          "access": {
            "$ref": "#/components/schemas/TokenAccess"
          },
          "scope": {
            "$ref": "#/components/schemas/TokenScope"
          },
          "created_at": {
            "type": "integer"
          },
          "last_used_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "revoked_at": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "example": {
          "id": "9e8d7c6b5a4f3e2d1c0b",
          "user_id": "1a2b3c4d5e6f708192a3b4c5",
          "name": "Claude",
          "prefix": "fpy_live_0f1e",
          "access": "read_write",
          "scope": "account",
          "created_at": 1762430580000,
          "last_used_at": null,
          "revoked_at": null
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request (`bad_request`). Fix the field named in `message`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No valid credentials. The router's sign-in check answers `{ \"error\": \"Sign in required\" }`; the drop endpoint answers with the full error shape and a `WWW-Authenticate: Bearer resource_metadata=\"https://fpy.sh/.well-known/oauth-protected-resource\"` header. See https://fpy.sh/auth.md.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Points at the RFC 9728 protected-resource metadata.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/PlainError"
                }
              ]
            },
            "example": {
              "error": "Sign in required"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The caller can't do this: a `read` key tried to write (`{ \"error\": \"This key can't do that\" }`), a key called a dashboard-only endpoint, a cookie request came from another site, or the account is suspended (`forbidden`).",
        "content": {
          "application/json": {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/PlainError"
                }
              ]
            },
            "example": {
              "error": "This key can't do that"
            }
          }
        }
      },
      "LimitReached": {
        "description": "Over a plan limit (`limit_reached`). Unclaimed accounts get `claim_url`: give it to your human, claiming lifts the agent limits. Claimed accounts get `upgrade_url`: your human upgrades to Pro there. Expiry over the plan's maximum on an unclaimed account carries only `next`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such link, share or file visible to this caller (`not_found`). An `own`-scope key only sees links it created. Never guess share links.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "Link not found",
                "next": "List your links with GET /api/v1/links.",
                "docs": "https://fpy.sh/docs/agents#errors"
              }
            }
          }
        }
      },
      "TooLarge": {
        "description": "Too big for one request (`limit_reached`): single uploads are capped at 100 MB. Use the multipart flow named in `next`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "limit_reached",
                "message": "Single uploads can be up to 100 MB.",
                "next": "For bigger files, create the link with POST /api/v1/links and upload parts. See https://fpy.sh/docs/agents#rest.",
                "docs": "https://fpy.sh/docs/agents#errors"
              }
            }
          }
        }
      },
      "FileBlocked": {
        "description": "Reserved (`file_blocked`): the file failed the safety check. Don't retry; `reason` says why. Today blocked uploads are accepted and then reported as `scan: \"blocked\"`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SlowDown": {
        "description": "Too many requests (`slow_down`). Wait `retry_after` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "slow_down",
                "message": "Too many requests. Wait a minute and try again.",
                "retry_after": 60,
                "next": "Retry after 60 seconds.",
                "docs": "https://fpy.sh/docs/agents#errors"
              }
            }
          }
        }
      }
    }
  }
}
