zfile

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:

  1. POST /api/v1/upload/init — kirim metadata file (nama, ukuran, hash), dapet balik signed upload URL.
  2. Upload file-nya langsung ke signed URL itu (pakai SDK Supabase storage, bahasa apa aja).
  3. 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

FieldWajibKeterangan
filenameYaNama file asli (dipakai buat nentuin ekstensi).
sizeYaUkuran file dalam bytes. Maks 50MB.
mimeTypeTidakDefault application/octet-stream.
expiryTidakSalah satu: never, 1h, 1d, 7d, 30d, 1y. Default never.
contentHashTidakHex 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

FieldWajibKeterangan
ticketYaTicket 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

StatusKapan terjadi
400Field wajib gak ada/invalid, ekstensi diblokir, atau ticket kedaluwarsa.
403IP kamu diblokir dari layanan ini.
413File lebih besar dari 50MB.
429Kena rate limit (per menit atau per jam). Cek header X-RateLimit-*.
500Error di server/storage. Coba lagi.
503API publik lagi penuh permintaan (cap global), atau situs lagi maintenance.
507Semua 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:

LapisanDefault limitYang terjadi kalau kena
Per IP, per menit3 request429 — cegah burst request beruntun.
Per IP, per jam8 request429. 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 request503 — 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.