Documentation
ZipStream is a high-performance service for creating zip files on-the-fly from multiple URLs. Instead of downloading files to a server first, ZipStream streams files directly from their sources to your download, minimizing memory usage and latency.
How It Works
- You send a JSON descriptor listing the files you want to include
- ZipStream fetches each file from its source URL
- Files are streamed directly into a zip archive
- The zip is streamed to your client as it's being created
This approach means:
- Low memory usage - Files are never fully stored on the server
- Fast response - Download starts immediately, no waiting for processing
- High concurrency - Handle many simultaneous requests efficiently
Quick Start
Create your first zip file with a simple POST request:
curl -X POST https://zipstream.app/api/downloads \
-H "Content-Type: application/json" \
-d '{
"suggestedFilename": "my-files.zip",
"files": [
{"url": "https://example.com/file1.jpg", "zipPath": "images/file1.jpg"},
{"url": "https://example.com/file2.pdf", "zipPath": "docs/file2.pdf"}
]
}' \
--output my-files.zip
JSON Descriptor Format
The JSON descriptor specifies the zip file contents:
{
"suggestedFilename": "archive.zip",
"files": [
{
"url": "https://example.com/image.jpg",
"zipPath": "images/photo.jpg"
}
]
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
suggestedFilename |
string | No | Filename suggested to the browser. Default: "archive.zip" |
files |
array | Yes | Array of file entries to include |
files[].url |
string | Yes | URL to fetch the file from (http or https) |
files[].zipPath |
string | Yes | Path within the zip archive (relative, no leading /) |
POST /api/downloads
Create and download a zip file immediately.
Request
POST /api/downloads
Content-Type: application/json
{
"suggestedFilename": "my-archive.zip",
"files": [
{"url": "https://example.com/file.jpg", "zipPath": "file.jpg"}
]
}
Response
Returns the zip file as a binary stream:
Content-Type: application/zipContent-Disposition: attachment; filename="my-archive.zip"
Error Responses
| Status | Description |
|---|---|
| 400 | Invalid JSON descriptor |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
GET /api/downloads
Create a zip file from a hosted JSON descriptor.
Query Parameters
| Parameter | Description |
|---|---|
descriptorUrl |
Full URL to the JSON descriptor file |
descriptorId |
ID appended to ZS_LISTFILE_URL_PREFIX |
Example
curl "https://zipstream.app/api/downloads?descriptorUrl=https://example.com/descriptor.json" \
--output archive.zip
POST /api/download-links
Create a temporary download link (valid for 60 seconds).
Request
Same JSON format as POST /api/downloads.
Response
{
"status": "ok",
"linkId": "b4ecfdb7-e0fa-4aca-ad87-cb2e4245c8dd",
"downloadUrl": "/api/download-links/b4ecfdb7-e0fa-4aca-ad87-cb2e4245c8dd",
"expiresIn": 60,
"expiresAt": 1234567890
}
GET /api/downloads_link/:linkId
Download a zip file using a previously created link.
Response
Returns the zip file as a binary stream, same as POST /api/downloads.
Errors
| Status | Description |
|---|---|
| 404 | Link has expired or is invalid |
Rate Limits
The free tier includes the following limits per IP address:
- 10 requests per hour
- 5 requests per 10 minutes (burst protection)
- 50 files per request maximum
- 5GB total size per request maximum
Rate Limit Headers
Every API response includes rate limit information:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per hour |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp when limit resets |
Handling Rate Limits (429 Too Many Requests)
When you exceed the rate limit, you'll receive a 429 Too Many Requests response:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please wait before trying again.",
"retry_after": 3600
}
Best Practices for Handling Rate Limits
- Check headers - Monitor
X-RateLimit-Remainingto avoid hitting limits - Implement backoff - Wait until
X-RateLimit-Resettimestamp before retrying - Queue requests - Space out requests to stay under burst limits
- Cache responses - Reuse download links where possible (valid for 60 seconds)
Need Higher Limits?
Premium tiers with higher rate limits are coming soon. Contact us if you need increased limits for your use case.
Code Examples
Examples in multiple languages for downloading files with ZipStream:
# Download directly
curl -X POST https://zipstream.app/api/downloads \
-H "Content-Type: application/json" \
-d '{"files": [{"url": "https://example.com/file.jpg", "zipPath": "file.jpg"}]}' \
--output archive.zip
# Create download link
curl -X POST https://zipstream.app/api/download-links \
-H "Content-Type: application/json" \
-d '{"files": [{"url": "https://example.com/file.jpg", "zipPath": "file.jpg"}]}'
# Check rate limit status
curl https://zipstream.app/api/rate-limits
async function downloadZip() {
const descriptor = {
suggestedFilename: 'my-files.zip',
files: [
{ url: 'https://example.com/image.jpg', zipPath: 'images/photo.jpg' },
{ url: 'https://example.com/doc.pdf', zipPath: 'documents/report.pdf' }
]
};
const response = await fetch('https://zipstream.app/api/downloads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(descriptor)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
// Download the blob
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'my-files.zip';
a.click();
URL.revokeObjectURL(url);
}
import requests
def download_zip():
descriptor = {
'suggestedFilename': 'my-files.zip',
'files': [
{'url': 'https://example.com/image.jpg', 'zipPath': 'images/photo.jpg'},
{'url': 'https://example.com/doc.pdf', 'zipPath': 'documents/report.pdf'}
]
}
response = requests.post(
'https://zipstream.app/api/downloads',
json=descriptor,
stream=True
)
response.raise_for_status()
with open('my-files.zip', 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print('Downloaded my-files.zip')
if __name__ == '__main__':
download_zip()
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
)
type FileEntry struct {
URL string `json:"url"`
ZipPath string `json:"zipPath"`
}
type Descriptor struct {
SuggestedFilename string `json:"suggestedFilename"`
Files []FileEntry `json:"files"`
}
func main() {
descriptor := Descriptor{
SuggestedFilename: "my-files.zip",
Files: []FileEntry{
{URL: "https://example.com/image.jpg", ZipPath: "images/photo.jpg"},
{URL: "https://example.com/doc.pdf", ZipPath: "documents/report.pdf"},
},
}
body, _ := json.Marshal(descriptor)
resp, err := http.Post(
"https://zipstream.app/api/downloads",
"application/json",
bytes.NewReader(body),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := os.Create("my-files.zip")
defer out.Close()
io.Copy(out, resp.Body)
}
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.json.JSONArray;
import org.json.JSONObject;
public class ZipStreamDownloader {
public static void main(String[] args) throws IOException {
// Create JSON descriptor
JSONObject descriptor = new JSONObject();
descriptor.put("suggestedFilename", "my-files.zip");
JSONArray files = new JSONArray();
files.put(new JSONObject()
.put("url", "https://example.com/image.jpg")
.put("zipPath", "images/photo.jpg"));
files.put(new JSONObject()
.put("url", "https://example.com/doc.pdf")
.put("zipPath", "documents/report.pdf"));
descriptor.put("files", files);
// Make POST request
URL url = new URL("https://zipstream.app/api/downloads");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
// Send JSON
try (OutputStream os = conn.getOutputStream()) {
byte[] input = descriptor.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
// Download response to file
try (InputStream is = conn.getInputStream();
FileOutputStream fos = new FileOutputStream("my-files.zip")) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("Downloaded my-files.zip");
}
}
<?php
$descriptor = [
'suggestedFilename' => 'my-files.zip',
'files' => [
[
'url' => 'https://example.com/image.jpg',
'zipPath' => 'images/photo.jpg'
],
[
'url' => 'https://example.com/doc.pdf',
'zipPath' => 'documents/report.pdf'
]
]
];
$ch = curl_init('https://zipstream.app/api/downloads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($descriptor),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
file_put_contents('my-files.zip', $response);
echo "Downloaded my-files.zip\n";
} else {
echo "Error: HTTP $httpCode\n";
echo $response . "\n";
}
?>
require 'net/http'
require 'json'
require 'uri'
descriptor = {
suggestedFilename: 'my-files.zip',
files: [
{
url: 'https://example.com/image.jpg',
zipPath: 'images/photo.jpg'
},
{
url: 'https://example.com/doc.pdf',
zipPath: 'documents/report.pdf'
}
]
}
uri = URI('https://zipstream.app/api/downloads')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
request.body = descriptor.to_json
response = http.request(request)
if response.code == '200'
File.open('my-files.zip', 'wb') do |file|
file.write(response.body)
end
puts 'Downloaded my-files.zip'
else
puts "Error: HTTP #{response.code}"
puts response.body
end
use reqwest;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::copy;
#[derive(Serialize, Deserialize)]
struct FileEntry {
url: String,
#[serde(rename = "zipPath")]
zip_path: String,
}
#[derive(Serialize, Deserialize)]
struct Descriptor {
#[serde(rename = "suggestedFilename")]
suggested_filename: String,
files: Vec<FileEntry>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let descriptor = Descriptor {
suggested_filename: "my-files.zip".to_string(),
files: vec![
FileEntry {
url: "https://example.com/image.jpg".to_string(),
zip_path: "images/photo.jpg".to_string(),
},
FileEntry {
url: "https://example.com/doc.pdf".to_string(),
zip_path: "documents/report.pdf".to_string(),
},
],
};
let client = reqwest::Client::new();
let response = client
.post("https://zipstream.app/api/downloads")
.json(&descriptor)
.send()
.await?;
let bytes = response.bytes().await?;
let mut file = File::create("my-files.zip")?;
copy(&mut bytes.as_ref(), &mut file)?;
println!("Downloaded my-files.zip");
Ok(())
}
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class ZipStreamDownloader
{
class FileEntry
{
public string url { get; set; }
public string zipPath { get; set; }
}
class Descriptor
{
public string suggestedFilename { get; set; }
public FileEntry[] files { get; set; }
}
static async Task Main(string[] args)
{
var descriptor = new Descriptor
{
suggestedFilename = "my-files.zip",
files = new[]
{
new FileEntry
{
url = "https://example.com/image.jpg",
zipPath = "images/photo.jpg"
},
new FileEntry
{
url = "https://example.com/doc.pdf",
zipPath = "documents/report.pdf"
}
}
};
using var client = new HttpClient();
var json = JsonSerializer.Serialize(descriptor);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://zipstream.app/api/downloads",
content
);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("my-files.zip", bytes);
Console.WriteLine("Downloaded my-files.zip");
}
}
import Foundation
struct FileEntry: Codable {
let url: String
let zipPath: String
}
struct Descriptor: Codable {
let suggestedFilename: String
let files: [FileEntry]
}
func downloadZip() async throws {
let descriptor = Descriptor(
suggestedFilename: "my-files.zip",
files: [
FileEntry(
url: "https://example.com/image.jpg",
zipPath: "images/photo.jpg"
),
FileEntry(
url: "https://example.com/doc.pdf",
zipPath: "documents/report.pdf"
)
]
)
let url = URL(string: "https://zipstream.app/api/downloads")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(descriptor)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let fileURL = FileManager.default.temporaryDirectory
.appendingPathComponent("my-files.zip")
try data.write(to: fileURL)
print("Downloaded my-files.zip to \(fileURL.path)")
}
// Usage (Swift 5.5+ with async/await)
Task {
do {
try await downloadZip()
} catch {
print("Error: \(error)")
}
}
# Create descriptor object
$descriptor = @{
suggestedFilename = "my-files.zip"
files = @(
@{
url = "https://example.com/image.jpg"
zipPath = "images/photo.jpg"
},
@{
url = "https://example.com/doc.pdf"
zipPath = "documents/report.pdf"
}
)
}
# Convert to JSON
$json = $descriptor | ConvertTo-Json -Depth 10
# Make POST request and save response
try {
Invoke-RestMethod `
-Uri "https://zipstream.app/api/downloads" `
-Method Post `
-ContentType "application/json" `
-Body $json `
-OutFile "my-files.zip"
Write-Host "Downloaded my-files.zip"
} catch {
Write-Error "Failed to download: $_"
}
# Alternative: Using Invoke-WebRequest for more control
<#
$response = Invoke-WebRequest `
-Uri "https://zipstream.app/api/downloads" `
-Method Post `
-ContentType "application/json" `
-Body $json
[System.IO.File]::WriteAllBytes("my-files.zip", $response.Content)
#>
Frequently Asked Questions
What file types are supported?
ZipStream supports any file type that can be accessed via HTTP/HTTPS URLs. The files are included in the zip exactly as they are served from their source.
Is there a size limit?
Free tier: Maximum 50 files per request and 5GB total size. Individual file sizes are limited by source server capabilities.
How long do download links last?
Download links created via /api/download-links expire after 60 seconds.
Can I use this commercially?
Yes! ZipStream is available for commercial use. For high-volume usage, please contact us about premium tiers.
How do I increase my rate limit?
Premium tiers with higher limits are coming soon. Contact us if you need higher limits immediately.