# moonpush.com > Free, temporary file sharing API. Upload any file up to 1 GB, get an instant download link. Built for AI agents, LLMs, developers, and automation. No signup required. ## Quick Start ```bash # Upload a file curl -F "file=@yourfile.pdf" https://moonpush.com/api/upload # Download a file curl -OJL https://moonpush.com/api/dl/ ``` --- ## API Endpoints Base URL: `https://moonpush.com` | Method | Endpoint | Description | |--------|----------|-------------| | POST | /api/upload | Upload a file | | POST | /api/upload/from-url | Import a file by URL (server-side fetch) | | POST | /api/upload/presign | Get a presigned PUT URL (browser → R2 direct) | | POST | /api/upload/presign/complete | Finalize a presigned upload | | POST | /api/upload/from-email | Inbound email → upload (provider webhook) | | POST,HEAD,PATCH,DELETE,OPTIONS | /api/tus[/{id}] | Resumable uploads (TUS 1.0.0) | | PUT | /api/account/webhook | Configure your upload webhook URL | | POST | /api/account/webhook-test | Send a synthetic upload.test event | | GET, HEAD | /api/dl/{id} | Download (or probe) a file by UUID | | GET | /api/share/{code} | Resolve a share link code | | POST | /api/folder/new | Create a folder | | GET | /api/folder/{id} | Get folder metadata and file list | | POST | /api/folder/attach | Attach an uploaded file (pipeId) to a folder | | GET | /api/folder-download/{id} | Download entire folder as .zip | | GET | /api/health | Service health check | --- ### POST /api/upload Upload a file. Accepts `multipart/form-data` with a `file` field. **curl:** ```bash curl -F "file=@report.csv" https://moonpush.com/api/upload ``` **Python:** ```python import requests with open("report.csv", "rb") as f: r = requests.post("https://moonpush.com/api/upload", files={"file": f}) data = r.json() print(data["downloadUrl"]) ``` **JavaScript/Node.js:** ```javascript const form = new FormData(); form.append("file", fs.createReadStream("report.csv")); const res = await fetch("https://moonpush.com/api/upload", { method: "POST", body: form, }); const data = await res.json(); console.log(data.downloadUrl); ``` **Response (201 Created):** ```json { "id": "9f6b14e5-c642-461c-a19e-6c348287c189", "shortCode": "3jhAiGZiz7S", "originalName": "report.csv", "mimeType": "text/csv", "size": 12048, "uploadedAt": "2026-03-15T05:35:33.322Z", "shareUrl": "https://moonpush.com/share/3jhAiGZiz7S", "downloadUrl": "https://moonpush.com/api/dl/9f6b14e5-c642-461c-a19e-6c348287c189", "directUrl": "https://cdn.moonpush.com/files/9f6b14e5-.../report.csv", "expiresIn": "30 minutes", "cli": "curl -OJL https://moonpush.com/api/dl/9f6b14e5-c642-461c-a19e-6c348287c189" } ``` **Which URL to use:** | Field | Use when | |-------|----------| | `shareUrl` | Sending the link to a **human** (browser-friendly preview page with download button + QR). Format: `https://moonpush.com/share/SHORTCODE` | | `downloadUrl` | **Programmatic / curl / agent** download. Direct file bytes with proper `Content-Disposition` and CORS. Format: `https://moonpush.com/api/dl/UUID` | | `directUrl` | Pre-signed CDN URL (faster, but no `Content-Disposition` header — file may save with wrong name). | | `cli` | Ready-to-paste curl command. | ## Encryption model (read this) MoonPush stores **only ciphertext** on R2 — across all three upload paths. The difference between paths is *when* the encryption happens, which determines the threat model. | Path | Storage | Server ever sees plaintext? | True E2E? | |------|---------|------------------------------|-----------| | `POST /api/upload` (curl, agents) | **AES-GCM 256 ciphertext** (encrypted server-side with an ephemeral key the server immediately forgets) | Yes, briefly during the upload request | No — "ephemeral-key encryption-at-rest" | | Browser upload at moonpush.com | **AES-GCM 256 ciphertext** | No, encryption happens in the browser | **Yes** | | MoonPush CLI (`moonpush upload`) | **AES-GCM 256 ciphertext** | No, encryption happens in the agent's process | **Yes** | What this means in practice: - A stolen R2 dump is useless to the thief in **all three** cases. - A subpoena for stored data returns ciphertext for **all three** cases. - A live-server compromise during the request window can read plaintext on the `/api/upload` path. Browser/CLI uploads are safe even then. ### Opting out of encryption (`?plain=true`) If you genuinely want plaintext storage (so any `curl -OJL ` works without a key), pass `?plain=true`: ```bash curl -F "file=@report.csv" "https://moonpush.com/api/upload?plain=true" ``` The response will have `encrypted: false` and `shareUrl` without a `#KEY` fragment. Anyone with the link can download readable bytes directly. ### Use the CLI for true end-to-end encryption from a terminal / agent ```bash # Install (one line) curl -sSL https://www.moonpush.com/cli/moonpush.mjs -o /usr/local/bin/moonpush && chmod +x /usr/local/bin/moonpush # Upload — encrypts locally, server only sees ciphertext moonpush upload secrets.pdf # → https://www.moonpush.com/share/abc123#KEY... # Download — fetches ciphertext, decrypts locally with #KEY moonpush download "https://www.moonpush.com/share/abc123#KEY..." out.pdf ``` The CLI requires Node.js 18+ or Bun. No npm install needed — single file, ~9 KB. Source: github.com/aamirmursleen/file-share/blob/main/public/cli/moonpush.mjs The encryption key is generated client-side and **never sent to MoonPush**. It lives only in the URL fragment which is never transmitted in HTTP requests. CLI-uploaded files can be decrypted in the browser at `/share/CODE#KEY` and vice versa — same chunked AES-GCM 256 format on both sides. **Implication for agents:** - A bare `https://moonpush.com/share/CODE` (no `#KEY`) → plaintext file uploaded via API. `GET /api/dl/{id}` returns readable bytes. - A `https://moonpush.com/share/CODE#KEY` (with fragment) → E2E ciphertext. `GET /api/dl/{id}` returns OPAQUE BYTES. To get plaintext, you must decrypt locally with the key from the fragment, OR open the URL in a browser which decrypts automatically. **Errors:** | Status | Reason | |--------|--------| | 400 | No file provided, empty file, blocked file type, or suspicious filename | | 413 | File too large | | 429 | Rate limit exceeded | --- ### POST /api/upload/from-url Import a file by URL — the server fetches the bytes and uploads them. Same response shape as `POST /api/upload` (plus a `sourceUrl` field). Useful for agents that already have a public URL (S3 link, GitHub release, screenshot host) and don't want to download then re-upload. **Request body** (`application/json`): ```json { "url": "https://example.com/report.pdf", "name": "renamed.pdf", // optional override "plain": false // optional, same as ?plain=true on /api/upload } ``` **curl:** ```bash curl -X POST https://moonpush.com/api/upload/from-url \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/report.pdf"}' ``` **SSRF protections:** http(s) only. The hostname is resolved to an IP and rejected if it falls in any private/loopback/link-local range (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ULA, etc.). Redirects are re-validated at every hop. Hard caps: 30s timeout, plan-defined max bytes. **One-time links** (also works on `/api/upload` via `?one_time=1` or `?max_downloads=N`): set `oneTime: true` or `maxDownloads: 5` in the body. After the cap is reached the share URL returns 410 Gone and the file is queued for deletion. **Status codes:** - `201 Created` — imported. Body has `{id, shareUrl, downloadUrl, sourceUrl, ...}` - `400` — invalid URL or fetch failed - `402` — insufficient credits (authenticated) - `413` — source file exceeds plan max - `429` — rate limit - `502` — source returned an error - `504` — fetch timed out --- ### POST /api/upload/presign Two-step direct-to-R2 upload. Use this when the file is large enough that streaming through our server is wasteful, or when you want the bytes to never touch our process at all (client-side encryption). ```bash # 1. Reserve an upload slot PRESIGN=$(curl -sS -X POST https://moonpush.com/api/upload/presign \ -H "Content-Type: application/json" \ -d '{"name":"big.zip","size":2147483648,"mime":"application/zip"}') ID=$(echo "$PRESIGN" | jq -r .id) URL=$(echo "$PRESIGN" | jq -r .uploadUrl) # 2. PUT the bytes directly to R2 (no proxy through our server) curl -X PUT -H "Content-Type: application/zip" --data-binary @big.zip "$URL" # 3. Finalize → get shareUrl + downloadUrl curl -X POST https://moonpush.com/api/upload/presign/complete \ -H "Content-Type: application/json" \ -d "{\"id\":\"$ID\"}" ``` The complete call HEADs the R2 object so a malicious client can't claim a phantom upload (we bill based on actual content-length, not their declared size). Note: presign mode does NOT use the server's ephemeral encryption-at-rest (we never see the bytes). For privacy, encrypt client-side and PUT the ciphertext, OR use `/api/upload` (server encrypts) or browser uploads at `/share/{code}` (true E2E). --- ### TUS resumable upload (TUS 1.0.0) Native [TUS protocol](https://tus.io) support. Use any TUS 1.0.0 client (`tus-js-client`, `Uppy`, `pytus`) and point it at `https://moonpush.com/api/tus`. Survives network drops, browser crashes, and machine sleep — the client just resumes from the last acknowledged offset. ```bash # Discovery curl -i -X OPTIONS https://moonpush.com/api/tus # → Tus-Resumable: 1.0.0; Tus-Extension: creation,termination # Create a 1 MiB upload LOC=$(curl -sSi -X POST https://moonpush.com/api/tus \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Length: 1048576" \ -H "Upload-Metadata: filename $(echo -n test.bin | base64)" \ | grep -i ^location | cut -d' ' -f2 | tr -d '\r') # Append a chunk curl -X PATCH "$LOC" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Offset: 0" \ -H "Content-Type: application/offset+octet-stream" \ --data-binary @chunk1.bin # → 204 No Content; Upload-Offset: 524288 # Resume after disconnect curl -I "$LOC" -H "Tus-Resumable: 1.0.0" # → Upload-Offset: 524288 (resume from this byte) ``` When the final chunk arrives the server returns `X-Share-Url` and `Location` headers (= `/api/dl/{pipeId}`). Files are encrypted-at-rest with an ephemeral key the server forgets. --- ### POST /api/upload/from-email Inbound-email webhook target. Wire your email provider (Cloudflare Email Routing → Worker, Postmark inbound, Resend inbound, Mailgun) to POST parsed messages here. Each attachment becomes a MoonPush share URL owned by the recipient address's user. **Operator setup:** 1. Set env `MOONPUSH_INBOUND_EMAIL_SECRET=` and `MOONPUSH_INBOUND_DOMAIN=inbox.moonpush.com`. 2. Point an MX record at your provider for `inbox.moonpush.com`. 3. Configure the provider to POST parsed inbound mail to `/api/upload/from-email` with header `x-moonpush-secret: `. Inbox alias: users send mail to `@inbox.moonpush.com`. ```json { "to": "ix_TDUgEaqn1nOa@inbox.moonpush.com", "from": "alice@example.com", "subject": "Photo", "attachments": [ { "filename": "photo.jpg", "contentType": "image/jpeg", "content": "" } ] } ``` --- ### SFTP gateway ```bash sftp -P 2222 you@sftp.moonpush.com # password: mp_live_… (your API key) sftp> put report.csv ``` Each `put` produces a share URL printed to the server log AND fires your configured webhook. Upload-only — `get` / `ls` are denied. Files encrypted at rest with an ephemeral key. **Operator setup:** ensure ssh2 is installed, expose port 2222, and (recommended) provide a persistent host key via `MOONPUSH_SFTP_HOST_KEY=/path/to/key.pem`. --- ### PUT /api/account/webhook Configure the webhook URL that fires after every successful upload — from any of the channels above. ```bash curl -X PUT https://moonpush.com/api/account/webhook \ -H "x-api-key: mp_live_…" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-app.com/webhooks/moonpush"}' # → { url, secret } (save the secret — shown once) ``` **Wire format** (Standard Webhooks / svix): ``` content-type: application/json webhook-id: webhook-timestamp: webhook-signature: v1= ``` Body: `{ "type": "upload.completed" | "upload.test", "data": { id, originalName, mimeType, size, encrypted, shareUrl, uploadedAt } }`. Send a test event: ```bash curl -X POST https://moonpush.com/api/account/webhook-test -H "x-api-key: mp_live_…" ``` --- ### GET /api/dl/{id} Download a file by its UUID. **The server never decrypts.** Bytes are returned as-stored: plaintext if uploaded via `/api/upload`, AES-GCM ciphertext if uploaded via the browser. For ciphertext, the recipient must decrypt locally using the `#KEY` from the share URL fragment. ```bash curl -OJL https://moonpush.com/api/dl/9f6b14e5-c642-461c-a19e-6c348287c189 ``` `-O` saves to disk, `-J` uses the server-provided filename, `-L` follows redirects. **Response headers:** - `Content-Type` — original MIME type (HTML is served as `text/plain` for security) - `Content-Disposition: attachment; filename="originalname.ext"` — forces download with original filename - `Access-Control-Allow-Origin: *` — CORS enabled for all origins - `X-Content-Type-Options: nosniff` — prevents MIME sniffing **Status codes:** | Status | Meaning | |--------|---------| | 200 | File downloaded successfully (plaintext OR ciphertext — see Encryption model above) | | 202 | File is still uploading — retry shortly | | 400 | Invalid file ID format (must be UUID v4) | | 404 | File not found (may have expired) | | 410 | File expired (30 min TTL exceeded) or upload failed | | 429 | Rate limit exceeded | | 502 | File data unavailable from storage | --- ### GET /api/share/{code} Resolve a share link short code to a file or folder ID. Share links look like `https://moonpush.com/share/ABC123` for plaintext API uploads, or `https://moonpush.com/share/ABC123#encryptionKey` for E2E browser uploads. **Do NOT ignore the fragment** — for E2E uploads it's the only way to decrypt the file. ```bash curl -s https://moonpush.com/api/share/ABC123 ``` **Response (file):** ```json { "type": "pipe", "pipeId": "9f6b14e5-c642-461c-a19e-6c348287c189", "status": "complete", "originalName": "report.csv", "mimeType": "text/csv", "size": 12048 } ``` **Response (folder):** ```json { "type": "folder", "folderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` **Status codes:** | Status | Meaning | |--------|---------| | 200 | Resolved successfully | | 404 | Code not found | | 410 | Link expired | --- ### Downloading files shared via browser (share links) When a user shares a file via the browser, the URL looks like: `https://moonpush.com/share/CODE#ENCRYPTION_KEY` **To download as an AI agent or from CLI:** ```bash # Step 1: Extract the code (part before #) CODE="ABC123" # Step 2: Resolve to get the file ID PIPE_ID=$(curl -s https://moonpush.com/api/share/$CODE | jq -r .pipeId) # Step 3: Download the file (server decrypts automatically) curl -OJL https://moonpush.com/api/dl/$PIPE_ID ``` **IMPORTANT for AI agents:** Do NOT try to fetch `https://moonpush.com/share/CODE` directly — that returns an HTML page, not the file. Always use the API: resolve via `/api/share/{code}`, then download via `/api/dl/{pipeId}`. **Python helper:** ```python import requests def download_share(share_url: str, save_path: str = None) -> str: # Extract code from URL (before # fragment) code = share_url.split("/share/")[-1].split("#")[0] # Resolve share code r = requests.get(f"https://moonpush.com/api/share/{code}") data = r.json() if data.get("type") == "folder": # Download folder as zip folder_id = data["folderId"] r = requests.get(f"https://moonpush.com/api/folder/{folder_id}/download") filename = save_path or "folder.zip" else: # Download single file pipe_id = data["pipeId"] r = requests.get(f"https://moonpush.com/api/dl/{pipe_id}") filename = save_path or data.get("originalName", "download") with open(filename, "wb") as f: f.write(r.content) return filename ``` --- ## Folders Folders let you group multiple files under a single shareable link. ### POST /api/folder/new Create a new folder. ```bash curl -s -X POST https://moonpush.com/api/folder/new \ -H "Content-Type: application/json" \ -d '{"name": "Project Files", "mode": "shared"}' ``` **Request body (all optional):** | Field | Type | Default | Description | |-------|------|---------|-------------| | name | string | "Untitled Folder" | Folder display name | | mode | string | "delivery" | Access mode: `delivery`, `shared`, or `submission` | | expiryMinutes | number | 30 | Auto-delete after N minutes | **Folder modes:** | Mode | Who can upload | Who can download | |------|---------------|-----------------| | delivery | Owner only | Anyone | | shared | Anyone | Anyone | | submission | Visitors only | Owner only | **Response (201 Created):** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "shortCode": "xK9mR2pL4nB", "folderUrl": "https://moonpush.com/folder/xK9mR2pL4nB", "ownerToken": "base64url-encoded-token", "name": "Project Files", "mode": "shared", "expiryMinutes": 30, "expiresAt": "2026-03-15T06:05:33.322Z" } ``` **Save the `ownerToken`** — you need it to manage the folder and download submission files. --- ### GET /api/folder/{id} Get folder metadata and file list. ```bash curl -s https://moonpush.com/api/folder/FOLDER_ID \ -H "x-owner-token: YOUR_TOKEN" ``` The `x-owner-token` header is optional but required to see files in submission-mode folders. **Response:** ```json { "id": "a1b2c3d4-...", "shortCode": "xK9mR2pL4nB", "name": "Project Files", "mode": "shared", "expiresAt": "2026-03-15T06:05:33.322Z", "isOwner": true, "fileCount": 3, "files": [ { "id": "file-uuid", "pipeId": "pipe-uuid", "originalName": "report.csv", "mimeType": "text/csv", "size": 12048, "status": "complete", "addedAt": "2026-03-15T05:35:33.322Z", "downloadUrl": "https://cdn.moonpush.com/files/pipe-uuid/report.csv" } ] } ``` To download individual files, use `GET /api/dl/{pipeId}` (not the `downloadUrl` CDN link, which lacks headers). --- ### POST /api/folder/attach Attach an existing uploaded file (from `/api/upload`) to a folder. This is the **recommended way** to add files to a folder — upload first, then attach. ```bash # Step 1: Upload a file curl -F "file=@report.csv" https://moonpush.com/api/upload # → returns { "id": "PIPE_ID", ... } # Step 2: Attach to folder curl -X POST https://moonpush.com/api/folder/attach \ -H "Content-Type: application/json" \ -H "x-owner-token: YOUR_TOKEN" \ -d '{"folderId": "FOLDER_ID", "pipeId": "PIPE_ID"}' ``` **Request body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | folderId | string | yes | UUID of the target folder | | pipeId | string | yes | UUID returned from POST /api/upload | **Response (201 Created):** ```json { "fileId": "file-uuid", "pipeId": "pipe-uuid", "folderId": "folder-uuid", "originalName": "report.csv", "size": 12048, "status": "complete" } ``` **Status codes:** | Status | Meaning | |--------|---------| | 201 | Attached successfully | | 400 | Missing/invalid folderId or pipeId | | 403 | Permission denied (folder mode restrictions) | | 404 | Folder or pipe not found | | 409 | File already attached to this folder | --- ### GET /api/folder-download/{id} Download all files in a folder as a single .zip archive. ```bash # Download folder as zip curl -OJL https://moonpush.com/api/folder-download/FOLDER_ID # For submission folders, include owner token curl -OJL https://moonpush.com/api/folder-download/FOLDER_ID \ -H "x-owner-token: YOUR_TOKEN" ``` **Response:** Binary .zip file with `Content-Disposition: attachment; filename="FolderName.zip"` **Status codes:** | Status | Meaning | |--------|---------| | 200 | Zip downloaded successfully | | 400 | No downloadable files in folder | | 403 | Submission folder — owner token required | | 404 | Folder not found | | 410 | Folder expired | | 429 | Rate limit exceeded | If some files failed to download, the zip will contain the files that succeeded and the `X-Skipped-Files` header indicates how many were skipped. --- ### Downloading a folder from a share link When a user shares a folder link like `https://moonpush.com/folder/CODE`: ```bash # Step 1: Resolve the code FOLDER_ID=$(curl -s https://moonpush.com/api/share/CODE | jq -r .folderId) # Step 2: Download as zip curl -OJL https://moonpush.com/api/folder-download/$FOLDER_ID ``` **Or download files individually:** ```bash # Step 1: Resolve the code FOLDER_ID=$(curl -s https://moonpush.com/api/share/CODE | jq -r .folderId) # Step 2: List files FILES=$(curl -s https://moonpush.com/api/folder/$FOLDER_ID | jq -r '.files[] | select(.status=="complete") | .pipeId') # Step 3: Download each file for PIPE_ID in $FILES; do curl -OJL https://moonpush.com/api/dl/$PIPE_ID done ``` --- ### GET /api/share/{code} Resolve a share link short code to a pipe ID. Share links look like `https://moonpush.com/share/ABC123#encryptionKey`. ```bash curl -s https://moonpush.com/api/share/ABC123 ``` **Response:** ```json { "type": "pipe", "pipeId": "9f6b14e5-c642-461c-a19e-6c348287c189" } ``` Then download the file: ```bash curl -OJL https://moonpush.com/api/dl/9f6b14e5-c642-461c-a19e-6c348287c189 ``` --- ### Downloading files shared via browser (share links) When a user shares a file via the browser, the URL looks like: `https://moonpush.com/share/CODE#ENCRYPTION_KEY` To download as an AI agent or from CLI: ```bash # Step 1: Resolve share code to pipe ID PIPE_ID=$(curl -s https://moonpush.com/api/share/CODE | jq -r .pipeId) # Step 2: Download the file (server decrypts automatically) curl -OJL https://moonpush.com/api/dl/$PIPE_ID ``` For E2E (browser-uploaded) pipes the server stores ciphertext only and cannot decrypt. To get plaintext, either (a) open the share URL in a browser, which decrypts client-side using the key from the URL fragment, or (b) fetch the ciphertext from `/api/dl/{id}` and decrypt locally with the AES-GCM key from the fragment. The encryption format (chunked AES-GCM with version header + base nonce) is documented in the open-source repo: github.com/aamirmursleen/file-share/blob/main/src/lib/crypto.ts --- ## Accepted File Types Most file types are accepted. Executables (.exe, .dll, .bat, .ps1, .vbs, etc.) are blocked for security. Common examples: | Category | Extensions | |----------|-----------| | Documents | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .rtf, .odt | | Images | .png, .jpg, .jpeg, .gif, .svg, .webp, .bmp, .ico, .tiff | | Video | .mp4, .mov, .avi, .mkv, .webm | | Audio | .mp3, .wav, .ogg, .flac, .m4a | | Code | .py, .js, .ts, .go, .rs, .java, .c, .cpp, .rb, .php, .sh | | Data | .csv, .json, .xml, .yaml, .toml, .sql, .parquet | | Archives | .zip, .tar, .gz, .7z, .rar, .tar.gz | | Other | .dmg, .iso, .wasm, .bin | --- ## Limits | Limit | Value | |-------|-------| | Max file size | 1 GB (100 GB for browser uploads) | | File types | Most types accepted. Executables blocked | | Expiry | 30 minutes after upload (auto-deleted) | | Authentication | None required | | Rate limiting | 15 uploads/hour, 50/day per IP | | Max filename length | 255 characters | | Max files per folder | 500 | --- ## Security | Protection | Details | |-----------|---------| | **No execution** | Files are stored as raw blobs. Never executed or processed. | | **UUID-only access** | Files accessible only via UUID v4 (122 bits of randomness). | | **Forced download** | `Content-Disposition: attachment` — never rendered in browser. | | **No MIME sniffing** | `X-Content-Type-Options: nosniff` header on all downloads. | | **HTML neutralized** | HTML files served as `text/plain` to prevent XSS. | | **Auto-expiry** | Files deleted after 30 minutes. No persistent storage. | | **CORS enabled** | `Access-Control-Allow-Origin: *` on all API responses. | --- ## Notes for AI Agents **Critical rules:** 1. The multipart form field name MUST be exactly `file`. 2. Files auto-delete after **30 minutes**. Do not rely on MoonPush for persistent storage. 3. There is **no endpoint to list or browse files**. Files are private and only accessible by UUID. 4. CORS is enabled (`Access-Control-Allow-Origin: *`) on all API endpoints. **Handling share links:** - When a user gives you a URL like `https://moonpush.com/share/CODE#KEY`, do NOT fetch the URL directly. It returns HTML. - Instead: (1) extract the code before `#`, (2) call `GET /api/share/{code}` to get the ID, (3) download via `GET /api/dl/{pipeId}` or `GET /api/folder-download/{folderId}`. - Check the `type` field in the share response: `"pipe"` = single file, `"folder"` = folder. **Handling folders:** - To download all files at once: `GET /api/folder-download/{id}` returns a .zip file. - To download files individually: get file list from `GET /api/folder/{id}`, then download each via `GET /api/dl/{pipeId}`. - For submission folders, include `x-owner-token` header. **Error handling:** - `404` = file/folder not found (may have expired) - `410` = explicitly expired - `202` = file still uploading, retry in a few seconds - `429` = rate limited, wait and retry **Complete example — download from share link:** ```python import requests def download_from_moonpush(url: str) -> str: code = url.split("/share/")[-1].split("/")[-1].split("#")[0] # Also handle /folder/ URLs if "/folder/" in url: code = url.split("/folder/")[-1].split("#")[0] info = requests.get(f"https://moonpush.com/api/share/{code}").json() if "error" in info: raise Exception(info["error"]) if info["type"] == "folder": r = requests.get(f"https://moonpush.com/api/folder-download/{info['folderId']}") filename = "folder.zip" else: r = requests.get(f"https://moonpush.com/api/dl/{info['pipeId']}") cd = r.headers.get("content-disposition", "") filename = info.get("originalName", "download") r.raise_for_status() with open(filename, "wb") as f: f.write(r.content) return filename ``` --- ## MCP Server An official MCP server is available on GitHub: [moonpush-mcp](https://github.com/aamirmursleen/moonpush-mcp) ```bash pip install "mcp[cli]" claude mcp add moonpush python3 /path/to/mcp-server/server.py ``` **Tools:** | Tool | Description | |------|-------------| | `upload_file(file_path)` | Upload a file from disk, returns download link | | `download_file(file_id, save_path?)` | Download a file by ID or URL | | `download_share(share_url, save_path?)` | Download from a share link — resolves and decrypts automatically | | `upload_text(content, filename?)` | Upload text/code as a file, returns download link | --- *Built by MoonPush.* --- *Last modified: 2026-06-16 12:01:49 UTC* Main company: CalendarJet Name: Sami