zfile API
API upload publik resmi, gak perlu API key. Endpoint ini di-versionkan (/v1/) supaya kontraknya tetap stabil ke depannya. Karena gak ada auth, dijaga rate limit berlapis — lihat bagian Rate Limit di bawah.
Base URL
https://zfile.web.id
Ganti sesuai domain kamu sendiri kalau meng-clone project ini (env NEXT_PUBLIC_SITE_URL).
Kenapa upload 2 tahap?
Server kita (Vercel Serverless Function) punya batas keras 4.5MB per request body yang gak bisa dinaikin di plan apapun. Supaya tetep bisa upload sampai 50MB, file-nya gak pernah lewat server kita sama sekali — browser/aplikasi kamu upload LANGSUNG ke Supabase Storage pakai signed URL sementara yang kita kasih. Alurnya:
POST /api/v1/upload/init— kirim metadata file (nama, ukuran, hash), dapet balik signed upload URL.- Upload file-nya langsung ke signed URL itu (pakai SDK Supabase storage, bahasa apa aja).
POST /api/v1/upload/finalize— kasih tau server upload-nya udah selesai, server verifikasi & kasih balik link final.
MCP Server resmi
ZFile menyediakan MCP Server resmi untuk aplikasi AI yang mendukung Model Context Protocol.
Server URL
https://zfile.web.id/api/mcp
Tool zfile_upload bisa mengupload file dari URL HTTP/HTTPS publik. File akan diproses lewat alur upload resmi ZFile, termasuk pengecekan ukuran, ekstensi, rate limit, dedupe, storage, dan finalize.
Batas upload MCP mengikuti ZFile: maksimal 50MB per file. MCP publik tidak memberikan akses ke IP hash, service-role key, data admin, ban, quarantine, atau operasi destruktif.
Tool MCP
zfile_upload— upload file dari URL publik.zfile_get_file— ambil metadata file publik.zfile_stats— statistik publik ZFile.zfile_docs— URL dokumentasi resmi.
Tahap 1: minta signed upload URL
POST /api/v1/upload/init
Content-Type: application/json
| Field | Wajib | Keterangan |
|---|---|---|
filename | Ya | Nama file asli (dipakai buat nentuin ekstensi). |
size | Ya | Ukuran file dalam bytes. Maks 50MB. |
mimeType | Tidak | Default application/octet-stream. |
expiry | Tidak | Salah satu: never, 1h, 1d, 7d, 30d, 1y. Default never. |
contentHash | Tidak | Hex SHA-256 dari isi file. Kalau diisi & ada file lama yang cocok & belum expired, server langsung balikin hasilnya tanpa perlu upload apa-apa lagi (lompat ke hasil akhir, gak perlu tahap 2 & 3). |
Contoh respons (200) — perlu upload
{
"deduped": false,
"url": "https://zfile.web.id/aB3xY9.png",
"slug": "aB3xY9",
"ext": "png",
"ticket": "eyJzbHVn...==.9f3a...",
"upload": {
"supabaseUrl": "https://xxxxx.supabase.co",
"anonKey": "eyJhbGciOi...",
"bucket": "uploads",
"path": "aB3xY9.png",
"token": "signed-upload-token-dari-supabase"
}
}url di sini udah bisa diprediksi dari slug & ekstensi, tapi file-nya BELUM TENTU ADA di storage sampai tahap 3 (/finalize) sukses — jangan dibagiin ke orang lain sebelum itu selesai.
ticket nyimpen hasil validasi tahap ini (slug, backend storage yang dipilih, expiry, dll), ditanda-tangani server. Simpen buat dipakai di tahap 3, jangan diotak-atik.
Contoh respons (200) — udah pernah diupload (deduped)
{
"deduped": true,
"url": "https://zfile.web.id/aB3xY9.png",
"slug": "aB3xY9",
"ext": "png",
"size_bytes": 204800,
"mime_type": "image/png",
"expires_at": null
}Kalau deduped: true, SELESAI di sini — gak perlu lanjut ke tahap 2 & 3, file-nya emang gak diupload ulang.
Tahap 2: upload ke signed URL
Field upload dari tahap 1 itu kredensial buat upload langsung ke Supabase Storage. Cara paling gampang & terjamin bener adalah pakai SDK resmi Supabase (tersedia buat JS, Python, Dart, Swift, Kotlin, dll) lewat method uploadToSignedUrl.
Contoh (JavaScript / Node.js)
import { createClient } from "@supabase/supabase-js";
const init = await fetch("https://zfile.web.id/api/v1/upload/init", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: "foto.png",
size: fileBuffer.length,
mimeType: "image/png",
expiry: "7d",
}),
}).then((r) => r.json());
if (init.deduped) {
console.log("Udah pernah ada:", init.url);
} else {
const supabase = createClient(init.upload.supabaseUrl, init.upload.anonKey);
const { error } = await supabase.storage
.from(init.upload.bucket)
.uploadToSignedUrl(init.upload.path, init.upload.token, fileBuffer, {
contentType: "image/png",
});
if (error) throw error;
const result = await fetch("https://zfile.web.id/api/v1/upload/finalize", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticket: init.ticket }),
}).then((r) => r.json());
console.log("Selesai:", result.url);
}Bukan pakai JS? Signed URL ini adalah fitur bawaan Supabase Storage (bukan sesuatu yang kita bikin sendiri) — pakai SDK Supabase buat bahasa kamu, method yang setara dengan uploadToSignedUrl. Kami sengaja gak nulis contoh curl/raw-HTTP di sini karena format request mentahnya bisa berubah tanpa pemberitahuan mengikuti versi Supabase Storage; SDK resmi selalu ngikutin perubahan itu otomatis.
Tahap 3: finalize
POST /api/v1/upload/finalize
Content-Type: application/json
| Field | Wajib | Keterangan |
|---|---|---|
ticket | Ya | Ticket yang didapat dari tahap 1. Berlaku 15 menit. |
Server verifikasi file-nya BENERAN ada di storage (bukan cuma percaya klaim) sebelum dicatat permanen.
Contoh respons sukses (200)
{
"url": "https://zfile.web.id/aB3xY9.png",
"slug": "aB3xY9",
"ext": "png",
"size_bytes": 204800,
"mime_type": "image/png",
"expires_at": "2027-08-24T12:00:00.000Z",
"deduped": false
}Boleh dipanggil berkali-kali dengan ticket yang sama (misal retry jaringan) — hasilnya idempoten, gak bakal ke-upload/tercatat dobel.
Error responses
| Status | Kapan terjadi |
|---|---|
| 400 | Field wajib gak ada/invalid, ekstensi diblokir, atau ticket kedaluwarsa. |
| 403 | IP kamu diblokir dari layanan ini. |
| 413 | File lebih besar dari 50MB. |
| 429 | Kena rate limit (per menit atau per jam). Cek header X-RateLimit-*. |
| 500 | Error di server/storage. Coba lagi. |
| 503 | API publik lagi penuh permintaan (cap global), atau situs lagi maintenance. |
| 507 | Semua storage backend penuh. Hubungi pemilik situs. |
Rate limit
Endpoint /api/v1/upload/init gak dilindungi Turnstile (gak ada browser di sisi pemanggil), jadi dijaga beberapa lapis rate limit sekaligus — dihitung dari semua percobaan init (berhasil ataupun ditolak), bukan cuma upload yang sukses:
| Lapisan | Default limit | Yang terjadi kalau kena |
|---|---|---|
| Per IP, per menit | 3 request | 429 — cegah burst request beruntun. |
| Per IP, per jam | 8 request | 429. Kalau tetep maksa nyoba sampai 3× lipat limit ini dalam 1 jam, IP otomatis diblokir sementara (default 24 jam). |
| Global, per jam (semua pemanggil API digabung) | 200 request | 503 — jaga-jaga kalau banyak IP berbeda nyerang bersamaan. |
Setiap respons menyertakan header X-RateLimit-Limit dan X-RateLimit-Remaining (berdasarkan limit per jam per IP) biar gampang dipantau dari sisi client. Semua angka di atas bisa diubah pemilik instance lewat env RATE_LIMIT_V1_PER_MINUTE, RATE_LIMIT_V1_PER_HOUR, dan RATE_LIMIT_V1_GLOBAL_PER_HOUR.
Halaman web (zfile.web.id) sendiri pakai endpoint internal terpisah dengan jatah lebih besar (30/jam) karena udah melewati verifikasi Turnstile di /verify.
Batasan lain
- Maks 50MB per file (hard limit Supabase free tier).
- Sebagian besar tipe file diterima, kecuali ekstensi yang bisa langsung dieksekusi (exe, msi, bat, sh, ps1, vbs, jar, apk, dan sejenisnya) — ditolak dengan status 400.
- Upload anonim, gak ada akun/API key.
- File dengan expiry otomatis dihapus setelah waktunya lewat.
- IP yang melanggar ketentuan bisa diblokir pemilik situs (status 403 kalau kena blokir).
- File HTML/XHTML/SVG diserve dengan
Content-Type: text/plain(bukan tipe aslinya) walau tersimpan dengan benar — ini proteksi biar file yang diupload gak bisa jalanin script di domain zfile (XSS/phishing lewat file hosting). Isinya tetap utuh, cuma ditampilin sebagai teks mentah, bukan dirender jadi halaman. - Akses ke link yang gak ada/expired: kalau request-nya minta
Accept: text/html(browser biasa), dapet halaman 404. Kalau lewat script/fetch/curl (Accept lain), dapet JSON:{"status": 404, "message": "..."}.
Baca juga ketentuan layanan buat aturan penggunaan lengkapnya.