{"openapi":"3.1.0","info":{"title":"PutPut API","version":"1.0","description":"Tee up your files. $0 egress, 10 GB free, no signup required.","contact":{"name":"PutPut","url":"https://putput.io","email":"support@putput.io"}},"servers":[{"url":"https://putput.io","description":"Production"}],"tags":[{"name":"Auth","description":"Authentication and token management"},{"name":"Upload","description":"File upload endpoints (presign, confirm, URL)"},{"name":"Files","description":"File listing, download, stats, and deletion"}],"paths":{"/api/v1/auth/guest":{"post":{"operationId":"createGuestToken","summary":"Create a guest token","description":"Creates a new guest account and returns a bearer token. No signup or email required. Guest accounts get 1 GB storage, 100 MB max file size, and files never expire. Rate limited to 5 tokens per IP per day.","tags":["Auth"],"security":[],"responses":{"200":{"description":"Guest token created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestTokenResponse"},"example":{"token":"pp_abc123def456ghi789","claim_url":"https://putput.io/login?claim=secret123","limits":{"storage_bytes":1073741824,"max_file_size_bytes":104857600,"max_files":-1,"expires_at":"2026-03-24T00:00:00.000Z"}}}}},"403":{"description":"IP address is banned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"FORBIDDEN","message":"Your IP address has been banned."}}}}},"429":{"description":"Rate limit exceeded (5 tokens per IP per day)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"RATE_LIMITED","message":"Too many guest tokens. Limit: 5 per day per IP.","hint":"Create a free account with an existing token to avoid this limit"}}}}}}}},"/api/v1/upload/presign":{"post":{"operationId":"presignUpload","summary":"Get a presigned upload URL","description":"Returns a presigned R2 URL for direct file upload. The client PUTs the file bytes to the presigned URL (no auth header needed), then calls POST /api/v1/upload/confirm. The presigned URL expires in 1 hour. Rate limited to 100 requests per token per hour.","tags":["Upload"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignRequest"},"example":{"filename":"photo.jpg","content_type":"image/jpeg","size_bytes":102400,"visibility":"public","prefix":"avatars","metadata":{"user_id":"123"},"tags":["profile","avatar"]}}}},"responses":{"200":{"description":"Presigned URL generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignResponse"},"example":{"upload_id":"up_abc123","presigned_url":"https://account.r2.cloudflarestorage.com/bucket/key?X-Amz-Signature=...","public_name":"avatars/up_abc123/photo.jpg","expires_at":"2026-02-22T01:00:00.000Z"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Invalid request body: filename: Required","hint":"Expected: { \"filename\": string, \"content_type\": string, \"size_bytes\": number }"}}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"UNAUTHORIZED","message":"Missing or invalid Authorization header.","hint":"Include header: Authorization: Bearer pp_..."}}}}},"403":{"description":"Content type blocked, file limit exceeded, or storage limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"STORAGE_LIMIT_EXCEEDED","message":"Guest storage limit reached (1 GB).","hint":"Visit your claim URL to upgrade"}}}}},"413":{"description":"File too large for current plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"FILE_TOO_LARGE","message":"File exceeds Guest limit of 100 MB. Upgrade to Free for 100 MB.","hint":"Visit your claim URL to upgrade"}}}}},"429":{"description":"Rate limit exceeded (100 per token per hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"RATE_LIMITED","message":"Too many upload requests. Limit: 100 per hour.","hint":"Wait and try again later"}}}}}}}},"/api/v1/upload/confirm":{"post":{"operationId":"confirmUpload","summary":"Confirm an upload","description":"Confirms that a file has been uploaded to R2 via the presigned URL. Verifies the file exists in storage, creates the file record, and returns the file with its public CDN URL. Must be called after successfully PUTting the file to the presigned URL.","tags":["Upload"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmRequest"},"example":{"upload_id":"up_abc123"}}}},"responses":{"200":{"description":"Upload confirmed, file record created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmResponse"},"example":{"file":{"id":"file_xyz789","original_name":"photo.jpg","public_name":"avatars/up_abc123/photo.jpg","public_url":"https://cdn.putput.io/user123/avatars/up_abc123/photo.jpg","content_type":"image/jpeg","size_bytes":102400,"visibility":"public","prefix":"avatars","metadata":{"user_id":"123"},"tags":["profile","avatar"],"expires_at":null,"created_at":"2026-02-22T00:00:00.000Z"}}}}},"400":{"description":"Invalid request body or file not yet uploaded to presigned URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"FILE_NOT_UPLOADED","message":"File not uploaded to storage yet.","hint":"PUT the file to the presigned_url before calling confirm"}}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Upload ID not found or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"NOT_FOUND","message":"Invalid or expired upload_id.","hint":"Call POST /api/v1/upload/presign first to get an upload_id"}}}}}}}},"/api/v1/upload/url":{"post":{"operationId":"uploadFromUrl","summary":"Upload a file from a URL","description":"Fetches a file from the given URL and stores it directly. No presign/PUT/confirm flow needed. The server downloads the file, so the URL must be publicly accessible. Max 100 MB. Rate limited to 100 requests per token per hour (shared with presign).","tags":["Upload"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlRequest"},"example":{"url":"https://example.com/photo.jpg","filename":"my-photo.jpg","visibility":"public","prefix":"imports","metadata":{"source":"example.com"},"tags":["imported"]}}}},"responses":{"200":{"description":"File uploaded successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmResponse"},"example":{"file":{"id":"file_xyz789","original_name":"my-photo.jpg","public_name":"imports/up_abc123/my-photo.jpg","public_url":"https://cdn.putput.io/user123/imports/up_abc123/my-photo.jpg","content_type":"image/jpeg","size_bytes":204800,"visibility":"public","prefix":"imports","metadata":{"source":"example.com"},"tags":null,"expires_at":null,"created_at":"2026-02-22T00:00:00.000Z"}}}}},"400":{"description":"Invalid request body, unreachable URL, or fetch failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"FETCH_FAILED","message":"Failed to fetch URL.","hint":"Ensure the URL is publicly accessible"}}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Content type blocked or storage/file limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File too large (100 MB limit for URL uploads)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"FILE_TOO_LARGE","message":"File exceeds 100 MB limit for URL uploads.","hint":"Use presigned upload for larger files"}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/files":{"get":{"operationId":"listFiles","summary":"List uploaded files","description":"Returns a paginated list of confirmed files for the authenticated user. Uses cursor-based pagination. Default page size is 50, max 100.","tags":["Files"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response for fetching the next page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Number of files per page (1-100). Defaults to 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"prefix","in":"query","required":false,"description":"Filter files by prefix (exact match).","schema":{"type":"string"}},{"name":"project_id","in":"query","required":false,"description":"Filter files by project ID.","schema":{"type":"string"}},{"name":"tag","in":"query","required":false,"description":"Filter files by tag (substring match).","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated file list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileListResponse"},"example":{"files":[{"id":"file_xyz789","original_name":"photo.jpg","public_name":"up_abc123/photo.jpg","public_url":"https://cdn.putput.io/user123/up_abc123/photo.jpg","content_type":"image/jpeg","size_bytes":102400,"visibility":"public","prefix":null,"metadata":null,"tags":null,"download_count":42,"expires_at":null,"project_id":null,"created_at":"2026-02-22T00:00:00.000Z"}],"cursor":"eyJjcmVhdGVkX2F0IjoiMjAyNi0wMi0yMlQwMDowMDowMC4wMDBaIiwiaWQiOiJmaWxlX3h5ejc4OSJ9","has_more":true}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/files/{id}":{"delete":{"operationId":"deleteFile","summary":"Delete a file","description":"Soft-deletes the file record and hard-deletes it from R2 storage. Decrements the user's storage usage. Cannot be undone.","tags":["Files"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The file ID (UUID) to delete.","schema":{"type":"string"}}],"responses":{"200":{"description":"File deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found or already deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"NOT_FOUND","message":"File not found or already deleted."}}}}}}}},"/api/v1/files/{id}/download":{"get":{"operationId":"downloadFile","summary":"Get a download URL for a file","description":"Returns a CDN URL for public files or a time-limited presigned URL for private files. Presigned URLs expire in 1 hour. Increments the file's download count.","tags":["Files"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The file ID (UUID) to download.","schema":{"type":"string"}}],"responses":{"200":{"description":"Download URL returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadResponse"},"example":{"download_url":"https://cdn.putput.io/user123/up_abc123/photo.jpg"}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found or deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"NOT_FOUND","message":"File not found or deleted."}}}}}}}},"/api/v1/files/{id}/stats":{"get":{"operationId":"getFileStats","summary":"Get file statistics","description":"Returns download count, file size, visibility, and creation date for a file.","tags":["Files"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The file ID (UUID) to get stats for.","schema":{"type":"string"}}],"responses":{"200":{"description":"File statistics returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileStats"},"example":{"id":"file_xyz789","download_count":42,"size_bytes":102400,"visibility":"public","created_at":"2026-02-22T00:00:00.000Z"}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found or deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"NOT_FOUND","message":"File not found or deleted."}}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"token","description":"API token starting with \"pp_\". Obtain one via POST /api/v1/auth/guest (no signup required) or create scoped tokens in the dashboard. Pass as: Authorization: Bearer pp_..."}},"schemas":{"ErrorResponse":{"type":"object","description":"Canonical error response shape used by all API errors.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code in SCREAMING_SNAKE_CASE.","examples":["UNAUTHORIZED","VALIDATION_ERROR","RATE_LIMITED","NOT_FOUND","FILE_TOO_LARGE","STORAGE_LIMIT_EXCEEDED","CONTENT_TYPE_BLOCKED","FILE_NOT_UPLOADED","FILE_LIMIT_EXCEEDED","MIME_MISMATCH","FETCH_FAILED","FORBIDDEN","SERVER_ERROR"]},"message":{"type":"string","description":"Human-readable error description."},"hint":{"type":"string","description":"Actionable suggestion for resolving the error. Optional."}},"required":["code","message"]}},"required":["error"]},"GuestTokenResponse":{"type":"object","description":"Response from creating a guest token.","properties":{"token":{"type":"string","description":"Bearer token starting with \"pp_\". Store securely -- it cannot be retrieved after creation.","examples":["pp_abc123def456ghi789"]},"claim_url":{"type":"string","format":"uri","description":"URL where the user can claim (upgrade) the guest account by providing an email.","examples":["https://putput.io/login?claim=secret123"]},"limits":{"type":"object","description":"Resource limits for this guest account.","properties":{"storage_bytes":{"type":"integer","description":"Maximum total storage in bytes (1073741824 = 1 GB).","examples":[1073741824]},"max_file_size_bytes":{"type":"integer","description":"Maximum size of a single file upload in bytes (104857600 = 100 MB).","examples":[104857600]},"max_files":{"type":"integer","description":"Maximum number of files. -1 means unlimited.","examples":[-1]},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 datetime when the guest account expires.","examples":["2026-03-24T00:00:00.000Z"]}},"required":["storage_bytes","max_file_size_bytes","max_files","expires_at"]}},"required":["token","claim_url","limits"]},"PresignRequest":{"type":"object","description":"Request body for getting a presigned upload URL.","properties":{"filename":{"type":"string","description":"Original filename. Sanitized server-side (safe chars only, max 200 chars).","examples":["photo.jpg"]},"content_type":{"type":"string","description":"MIME type of the file. Guest tokens cannot upload text/html, text/javascript, text/css, application/xml, application/xhtml+xml, or image/svg+xml.","examples":["image/jpeg"]},"size_bytes":{"type":"integer","description":"Exact file size in bytes. Checked against plan limits.","examples":[102400]},"visibility":{"type":"string","enum":["public","private"],"default":"public","description":"File visibility. \"private\" requires a Pro plan."},"prefix":{"type":"string","maxLength":200,"description":"Optional path prefix for organizing files (e.g. \"avatars\", \"documents/2026\").","examples":["avatars"]},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":1024},"maxProperties":10,"description":"Key-value metadata. Max 10 keys, key max 256 chars, value max 1024 chars."},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":10,"description":"Tags for categorization. Max 10 tags, 50 chars each."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 datetime when the file should be automatically deleted. Must be in the future."}},"required":["filename","content_type","size_bytes"]},"PresignResponse":{"type":"object","description":"Response containing the presigned URL and upload metadata.","properties":{"upload_id":{"type":"string","description":"Upload ID. Pass this to POST /api/v1/upload/confirm after uploading."},"presigned_url":{"type":"string","format":"uri","description":"Presigned R2 URL. PUT the file here with the matching Content-Type header. No Authorization header needed."},"public_name":{"type":"string","description":"Server-assigned public path for the file (e.g. \"prefix/upload_id/filename\")."},"expires_at":{"type":"string","format":"date-time","description":"When the presigned URL expires (1 hour from creation)."}},"required":["upload_id","presigned_url","public_name","expires_at"]},"ConfirmRequest":{"type":"object","description":"Request body for confirming an upload.","properties":{"upload_id":{"type":"string","description":"The upload_id from the presign response."}},"required":["upload_id"]},"ConfirmResponse":{"type":"object","description":"Response wrapping a confirmed file. Used by both upload/confirm and upload/url.","properties":{"file":{"$ref":"#/components/schemas/FileItem"}},"required":["file"]},"UploadUrlRequest":{"type":"object","description":"Request body for uploading a file from a URL.","properties":{"url":{"type":"string","format":"uri","description":"URL to fetch the file from. Must be publicly accessible.","examples":["https://example.com/photo.jpg"]},"filename":{"type":"string","minLength":1,"maxLength":255,"description":"Override filename. If omitted, inferred from the URL path."},"visibility":{"type":"string","enum":["public","private"],"default":"public","description":"File visibility. \"private\" requires a Pro plan."},"prefix":{"type":"string","maxLength":200,"description":"Optional path prefix for organizing files."},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":1024},"maxProperties":10,"description":"Key-value metadata. Max 10 keys."},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":10,"description":"Tags for categorization."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 datetime when the file should be automatically deleted."}},"required":["url"]},"FileItem":{"type":"object","description":"A file stored in PutPut. Returned in file listings and after upload confirmation.","properties":{"id":{"type":"string","description":"Unique file identifier (UUID)."},"original_name":{"type":"string","description":"Original filename as provided during upload."},"public_name":{"type":"string","description":"Server-assigned unique path used in the CDN URL."},"public_url":{"type":["string","null"],"format":"uri","description":"Public CDN URL. null for private files -- use GET /api/v1/files/{id}/download for a presigned URL."},"content_type":{"type":"string","description":"MIME type of the file."},"size_bytes":{"type":"integer","description":"File size in bytes."},"visibility":{"type":"string","enum":["public","private"],"description":"File visibility."},"prefix":{"type":["string","null"],"description":"Path prefix if set during upload."},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"User-defined key-value metadata. null if not set."},"tags":{"type":["array","null"],"items":{"type":"string"},"description":"User-defined tags. null if not set."},"download_count":{"type":"integer","description":"Number of times this file has been downloaded."},"expires_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 datetime when this file expires. null if no expiry."},"project_id":{"type":["string","null"],"description":"Project ID if the file belongs to a project."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 datetime when the file was uploaded."}},"required":["id","original_name","public_name","public_url","content_type","size_bytes","visibility","created_at"]},"FileListResponse":{"type":"object","description":"Paginated list of files.","properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/FileItem"},"description":"Array of files matching the query."},"cursor":{"type":["string","null"],"description":"Opaque cursor for fetching the next page. null when there are no more pages."},"has_more":{"type":"boolean","description":"true if there are more files beyond this page."}},"required":["files","cursor","has_more"]},"DownloadResponse":{"type":"object","description":"Response containing a download URL for a file.","properties":{"download_url":{"type":"string","format":"uri","description":"CDN URL for public files, or a time-limited presigned URL for private files."},"expires_at":{"type":"string","format":"date-time","description":"When the presigned URL expires. Only present for private files."}},"required":["download_url"]},"FileStats":{"type":"object","description":"Statistics for a single file.","properties":{"id":{"type":"string","description":"Unique file identifier (UUID)."},"download_count":{"type":"integer","description":"Total number of times this file has been downloaded."},"size_bytes":{"type":"integer","description":"File size in bytes."},"visibility":{"type":"string","enum":["public","private"],"description":"File visibility."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 datetime when this file was uploaded."}},"required":["id","download_count","size_bytes","visibility","created_at"]},"UploadResult":{"type":"object","description":"Simplified upload result as returned by the SDK. Maps from the full FileItem.","properties":{"id":{"type":"string","description":"Unique file identifier (UUID)."},"url":{"type":["string","null"],"format":"uri","description":"Public CDN URL. null if visibility is private."},"original_name":{"type":"string","description":"Original filename."},"public_name":{"type":"string","description":"Server-assigned unique path."},"content_type":{"type":"string","description":"MIME type."},"size_bytes":{"type":"integer","description":"File size in bytes."},"visibility":{"type":"string","enum":["public","private"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}}},"required":["id","url","original_name","public_name","content_type","size_bytes","visibility"]}}}}