# PutPut API ## Base URL https://putput-ala.pages.dev/api ## Authentication All endpoints except `POST /api/auth/guest` require: `Authorization: Bearer ` ## File object Every endpoint that returns file data uses this shape: ```typescript interface File { id: string; // e.g. "file_xyz" original_name: string; // original filename, e.g. "photo.jpg" public_name: string; // storage path, e.g. "abc123/photo.jpg" public_url: string; // full CDN URL — use this to serve the file (NOT "url") content_type: string; // MIME type, e.g. "image/jpeg" size_bytes: number; // file size in bytes (NOT "size" or "filesize") created_at: string; // ISO 8601 timestamp } ``` The confirm response wraps the file in a `file` key — unwrap it: `const { file } = await res.json()`. ## Complete upload example (JavaScript) ```javascript const BASE = "https://putput-ala.pages.dev"; // Step 1: get a token (reuse if already stored — namespace the key to avoid localhost collisions) let token = localStorage.getItem("myapp:putput_token"); if (!token) { const { token: newToken } = await fetch(`${BASE}/api/auth/guest`, { method: "POST" }).then(r => r.json()); token = newToken; localStorage.setItem("myapp:putput_token", token); } // Step 2: presign — fileBlob is a File or Blob object const { upload_id, presigned_url } = await fetch(`${BASE}/api/upload/presign`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ filename: "photo.jpg", content_type: "image/jpeg", size_bytes: fileBlob.size }), }).then(r => r.json()); // Step 3: upload directly to storage (no auth header — this goes straight to R2) await fetch(presigned_url, { method: "PUT", headers: { "Content-Type": "image/jpeg" }, body: fileBlob, }); // Step 4: confirm — unwrap the { file } envelope const { file } = await fetch(`${BASE}/api/upload/confirm`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ upload_id }), }).then(r => r.json()); console.log(file.public_url); // ← the URL to serve/store ``` ## Endpoints ### POST /api/auth/guest Get a token. No signup required. ```bash curl -X POST https://putput-ala.pages.dev/api/auth/guest ``` Returns: ```json { "token": "pp_...", "claim_url": "https://putput-ala.pages.dev/login?claim=...", "limits": { "storage_bytes": -1, "max_file_size_bytes": -1, "max_files": -1, "expires_at": null } } ``` `-1` in limits means unlimited. Save the `token` and `claim_url`. The claim_url lets the user upgrade to a free account later. ### POST /api/upload/presign Get a presigned URL to upload a file directly to storage. ```bash curl -X POST https://putput-ala.pages.dev/api/upload/presign \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 102400}' ``` Returns: ```json { "upload_id": "abc123", "presigned_url": "https://r2.cloudflarestorage.com/...", "public_name": "abc123/photo.jpg", "expires_at": "2025-01-01T01:00:00.000Z" } ``` ### PUT Upload the file directly to the presigned URL. The `Content-Type` header must match the `content_type` from the presign request. ```bash curl -X PUT "" -H "Content-Type: image/jpeg" --data-binary @photo.jpg ``` Returns: empty body, HTTP 200 on success. ### POST /api/upload/confirm Confirm the upload. Returns the public URL. ```bash curl -X POST https://putput-ala.pages.dev/api/upload/confirm \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"upload_id": "abc123"}' ``` Returns: ```json { "file": { "id": "file_xyz", "original_name": "photo.jpg", "public_name": "abc123/photo.jpg", "public_url": "https://cdn.example.com/abc123/photo.jpg", "content_type": "image/jpeg", "size_bytes": 102400, "created_at": "2025-01-01T00:00:00.000Z" } } ``` ### GET /api/files List uploaded files. Supports cursor pagination: `?cursor=&limit=` ```bash curl https://putput-ala.pages.dev/api/files -H "Authorization: Bearer " ``` Returns: ```json { "files": [ { "id": "file_xyz", "original_name": "photo.jpg", "public_name": "abc123/photo.jpg", "public_url": "https://cdn.example.com/abc123/photo.jpg", "content_type": "image/jpeg", "size_bytes": 102400, "created_at": "2025-01-01T00:00:00.000Z" } ], "cursor": "file_abc", "has_more": true } ``` ### DELETE /api/files/:id Delete a file. ```bash curl -X DELETE https://putput-ala.pages.dev/api/files/ -H "Authorization: Bearer " ``` Returns: ```json { "success": true } ``` ## Storing tokens Persist the token in localStorage so users don't lose it on refresh. **Always namespace the key with your app name** — multiple apps running on localhost share the same origin and will overwrite each other's tokens if you use a generic key: ```javascript // Good — app-specific key, survives refresh, won't collide with other apps on localhost localStorage.setItem('myapp:putput_token', token); const token = localStorage.getItem('myapp:putput_token'); // Bad — collides with every other PutPut integration on the same origin localStorage.setItem('putput_token', token); ``` Replace `myapp` with your actual app name or any unique prefix. ## Upgrading The `claim_url` from token creation lets the user upgrade from guest to free. Show them the URL, they enter their email, done. The token string stays the same. ## Errors ```json { "error": { "code": "ERROR_CODE", "message": "..." } } ``` | Code | HTTP | Meaning | |------|------|---------| | UNAUTHORIZED | 401 | Missing or invalid token | | VALIDATION_ERROR | 400 | Invalid request body (missing or wrong fields) | | UPLOAD_NOT_FOUND | 404 | Invalid or expired upload_id | | FILE_NOT_FOUND | 404 | File doesn't exist or already deleted | | FILE_NOT_UPLOADED | 400 | File wasn't uploaded to storage yet | | FILE_TOO_LARGE | 400 | File exceeds max size for account tier | | FILE_LIMIT_EXCEEDED | 403 | File count limit reached for account tier | | STORAGE_LIMIT_EXCEEDED | 403 | Storage quota exceeded | | CONTENT_TYPE_BLOCKED | 403 | Content type not allowed for guest accounts | | RATE_LIMITED | 429 | Too many requests |