Jobs API
API reference for creating and managing ZIP jobs.
A job fetches remote files and packages them into one or more ZIPs. It
produces one ZIP by default; set max_zip_size_bytes to auto-split
the input across N ZIP files, each capped at that size.
Both shapes share the same job lifecycle and the same zips[] array in the
response.
Create Job
POST /jobsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
files | { url: string; filename?: string }[] | Yes | Array of file entries to download. Each entry requires url and can optionally include filename. Min 1; max depends on your plan and mode. |
mode | "stored" | "stream" | No | stored (default) builds the ZIP once and serves it from R2. stream builds the ZIP on the fly at download time. |
zip_filename | string | No | Filename for the ZIP. Supports the filename template syntax for split jobs. Max 255 characters. Defaults to archive-{id}.zip. |
expires_in | number | No | Seconds until the ZIP is deleted. Min 300 (5 min), max depends on your plan. Defaults to 24 hours. |
fail_on_url_error | boolean | No | If true (default), the job fails when any URL cannot be fetched. If false, failed URLs are skipped and recorded in errors. |
metadata | object | No | Up to 10 key/value pairs (key ≤ 128 chars, value ≤ 1,024 chars) stored with the job. |
max_zip_size_bytes | number | No | Enable auto-split: each output ZIP is capped at this many bytes. Min 104857600 (100 MB), max 536870912000 (500 GB). Omit (or pass null) to produce a single ZIP. |
split_strategy | "preserve-order" | "ffd" | No | Bin-packing strategy when max_zip_size_bytes is set. preserve-order (default) keeps the input order across bins. ffd (First-Fit-Decreasing) minimises the number of bins by sorting by size. |
allow_oversize_zips | boolean | No | If true (default), a single file larger than max_zip_size_bytes is placed in its own ZIP that exceeds the cap. Pass false to fail the job instead. |
fail_on_zip_error | boolean | No | If true (default), the job is marked failed when any individual ZIP fails to build. If false, completed ZIPs remain downloadable and only the failing ones are reported in errors. |
Plan Limits
| Free | Starter | Pro | Scale | |
|---|---|---|---|---|
| Stored files per job | 100 | 1,000 | 5,000 | 20,000 |
| Stream files per job | 100 | 1,000 | 5,000 | 20,000 |
| Max total output per job | 5 GB | 25 GB | 100 GB | 500 GB |
| Max size per output archive | 50 GB | 50 GB | 50 GB | 50 GB |
| Stored quota | 100 GB-days/month | 3,000 GB-days/month | 20,000 GB-days/month | 75,000 GB-days/month |
| Stream quota | 20 GB/month | 500 GB/month | 3 TB/month | 20 TB/month |
Max retention (expires_in) | 86,400 s (24 h) | 604,800 s (7 d) | 5,184,000 s (60 d) | 7,776,000 s (90 d) |
max_zip_size_bytes is the per-archive cap used when auto-splitting, and
must be at most 50 GB. Omit it and Eazip creates one archive under both the
uniform 50 GB archive guardrail and the plan's total-output limit.
Prefer stored mode for larger or repeated downloads.
Example Request
curl -X POST https://api.eazip.io/jobs \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"files": [
{
"url": "https://example.com/file1.pdf",
"filename": "invoice-001.pdf"
},
{
"url": "https://example.com/file2.pdf"
},
{
"url": "https://example.com/image.png",
"filename": "preview.png"
}
],
"zip_filename": "my-archive",
"expires_in": 172800,
"fail_on_url_error": false,
"metadata": {
"order_id": "ord_123"
}
}'Example: Auto-Split
Cap each output ZIP at 2 GB. The job is split into N ZIPs at or below the
cap, with auto-numbered filenames (photos_1.zip, photos_2.zip, ...).
curl -X POST https://api.eazip.io/jobs \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"files": [
{ "url": "https://example.com/photo-001.jpg" },
{ "url": "https://example.com/photo-002.jpg" }
],
"zip_filename": "photos.zip",
"max_zip_size_bytes": 2147483648,
"split_strategy": "preserve-order",
"fail_on_zip_error": true
}'Response
HTTP 201 Created
{
"success": true,
"job_id": "550e8400-e29b-41d4-a716-446655440000"
}An auto-split job briefly enters status: "preparing" while Eazip probes each
URL with HEAD and runs the bin-packing algorithm. It then moves to pending
/ processing and zips[] is populated.
List Jobs
GET /jobsUses cursor-based pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | number | Results per page. Default 20, max 100. |
cursor | string | Opaque cursor from meta.next_cursor in the previous response. Omit for the first page. |
status | string | Filter by status: pending, processing, completed, failed. |
Example Request
curl "https://api.eazip.io/jobs?status=completed&limit=10" \
-H "X-API-Key: YOUR_API_KEY"Response
{
"success": true,
"jobs": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"url_count": 3,
"file_count": 3,
"zip_filename": "my-archive.zip",
"zip_size": 1048576,
"multi_zip": false,
"zip_count": 1,
"total_size": 1048576,
"max_zip_size_bytes": null,
"fail_on_url_error": true,
"created_at": "2025-01-21T10:00:00.000Z",
"completed_at": "2025-01-21T10:00:45.000Z",
"expires_at": "2025-01-23T10:00:00.000Z"
}
],
"meta": {
"limit": 10,
"total": 42,
"next_cursor": "2025-01-20T09:00:00.000Z",
"has_more": true
}
}For split jobs, zip_size / zip_filename surface the first ZIP bin only,
while zip_count, total_size, and multi_zip
describe the job as a whole. Call GET /jobs/:id for the full zips[] array.
Pass meta.next_cursor as the cursor query parameter for the next page.
meta.has_more: false means you have reached the end.
Get Job
GET /jobs/:idExample Request
curl https://api.eazip.io/jobs/550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: YOUR_API_KEY"Response (single ZIP)
For a non-split job (no max_zip_size_bytes), zips[] holds a single entry —
read zips[0] for the filename, size, and download URL.
{
"success": true,
"job": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"files": [
{ "url": "https://example.com/file1.pdf", "filename": "invoice-001.pdf" },
{ "url": "https://example.com/file2.pdf", "filename": null },
{ "url": "https://example.com/image.png", "filename": "preview.png" }
],
"url_count": 3,
"file_count": 3,
"zip_filename": "my-archive.zip",
"zip_size": 1048576,
"fail_on_url_error": true,
"download_url": "https://api.eazip.io/download/eyJ...",
"metadata": { "order_id": "ord_123" },
"created_at": "2025-01-21T10:00:00.000Z",
"completed_at": "2025-01-21T10:00:45.000Z",
"expires_at": "2025-01-23T10:00:00.000Z",
"multi_zip": false,
"max_zip_size_bytes": null,
"split_strategy": "preserve-order",
"fail_on_zip_error": true,
"zip_count": 1,
"total_size": 1048576,
"zips": [
{
"id": "zip_a1b2c3d4e5f6",
"sequence": 1,
"status": "completed",
"filename": "my-archive.zip",
"file_count": 3,
"size": 1048576,
"download_url": "https://api.eazip.io/download/eyJ..."
}
]
}
}Response (auto-split)
When max_zip_size_bytes is set, multi_zip is true and the top-level
download_url / zip_size are null. Each zips[] entry has its own signed
download_url.
{
"success": true,
"job": {
"id": "9c4d2f8e-1234-5678-90ab-cdef01234567",
"status": "completed",
"files": [ /* 1000 entries */ ],
"url_count": 1000,
"file_count": 1000,
"zip_filename": "photos_01.zip",
"zip_size": null,
"fail_on_url_error": true,
"download_url": null,
"metadata": null,
"created_at": "2026-04-22T10:00:00.000Z",
"completed_at": "2026-04-22T10:08:12.000Z",
"expires_at": "2026-04-23T10:00:00.000Z",
"multi_zip": true,
"max_zip_size_bytes": 2147483648,
"split_strategy": "preserve-order",
"fail_on_zip_error": true,
"zip_count": 2,
"total_size": 3221225472,
"zips": [
{
"id": "zip_aaa111",
"sequence": 1,
"status": "completed",
"filename": "photos_01.zip",
"file_count": 612,
"size": 2147483648,
"download_url": "https://api.eazip.io/download/eyJ...AAA"
},
{
"id": "zip_bbb222",
"sequence": 2,
"status": "completed",
"filename": "photos_02.zip",
"file_count": 388,
"size": 1073741824,
"download_url": "https://api.eazip.io/download/eyJ...BBB"
}
]
}
}download_url (top-level and per-zip) is present only when status is
completed and the job has not yet expired.
zips[] entry fields
| Field | Type | Description |
|---|---|---|
id | string | ZIP file identifier (zip_...). |
sequence | number | 1-indexed bin number. |
status | string | pending, processing, completed, or failed. Each ZIP has an independent lifecycle. |
filename | string | Filename for this bin (see filename templates). |
file_count | number | Number of files packaged into this bin. |
size | number | null | Final ZIP size in bytes. null until packaging completes. |
download_url | string | null | Signed download URL. null for non-completed bins. |
errors | array | Per-bin URL errors (only present when applicable). |
When fail_on_url_error is false and some fetches fail, the response
includes an errors array at the job level and per-bin in zips[*].errors:
{
"success": true,
"job": {
"id": "...",
"status": "completed",
"errors": [
{ "url": "https://example.com/missing.pdf", "error": "HTTP 404" }
]
}
}Job Status Values
| Status | Description |
|---|---|
preparing | Auto-split jobs only: probing URLs and computing the bin layout. |
pending | Job (or one of its ZIP bins) waiting to start. |
processing | Fetching files and building one or more ZIPs. |
completed | All ZIPs are ready for download. |
failed | Job failed — check errors (job-level and per-zip) for details. |
status is the aggregate across all ZIP bins. With
fail_on_zip_error: false the job may finish completed even if some bins
failed, and the surviving bins stay downloadable from zips[].
ZIP Filename Templates
zip_filename accepts either a plain string or a template containing {...}
placeholders, for both single-ZIP and auto-split jobs.
Plain mode (no {...})
| Case | Output |
|---|---|
Single ZIP (max_zip_size_bytes not set) | zip_filename used as-is. |
Single ZIP, zip_filename omitted | archive-{job_id}.zip. |
| Split into N ≥ 2 ZIPs | _{n} is inserted before the last extension, zero-padded to the digit count of N. |
| Split, no extension | .zip is appended automatically. |
zip_filename: "abc.zip"
N=1 → abc.zip
N=5 → abc_1.zip ... abc_5.zip (1 digit)
N=15 → abc_01.zip ... abc_15.zip (2 digits)
N=150 → abc_001.zip ... abc_150.zip (3 digits)
zip_filename: "photos.tar.gz"
N=5 → photos.tar_1.gz ... photos.tar_5.gz (insert before last dot)
zip_filename: "abc" (no extension)
N=5 → abc_1.zip ... abc_5.zipTemplate mode (contains {...})
When zip_filename contains a placeholder, Eazip treats the value as a
literal template and expands it.
| Placeholder | Meaning | Example (n=2, N=5) |
|---|---|---|
{n} | 1-indexed bin number, no padding | 2 |
{n2} | Bin number, zero-padded to 2 digits | 02 |
{n3} | Bin number, zero-padded to 3 digits | 002 |
{total} | Total number of ZIPs | 5 |
{date} | Job creation date in UTC (YYYY-MM-DD) | 2026-04-23 |
Use {{ and }} to render literal braces.
"abc_{n}.zip" → abc_1.zip, abc_2.zip, ..., abc_5.zip
"abc_{n2}.zip" → abc_01.zip, abc_02.zip, ..., abc_05.zip
"photos-{n}-of-{total}.zip" → photos-1-of-5.zip, ..., photos-5-of-5.zip
"{date}-backup-{n2}.zip" → 2026-04-23-backup-01.zip, ...
"export.{n3}.zip" → export.001.zip, export.002.zip, ...If a template has no index placeholder ({n} / {n2} / {n3}) but the job
splits into multiple ZIPs, Eazip renders the template once and then applies
the plain-mode _{n} rule to guarantee unique filenames.
Validation:
- Unknown placeholders (e.g.
{banana}) return400. - Unbalanced braces return
400. - The expanded filename per bin must be 1–255 characters and must not contain
/or\.
Retry Job
Retry a job with status: "failed" — no other status can be retried. By
default it resumes from the last checkpoint.
POST /jobs/:id/retryRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
from_checkpoint | boolean | No | Resume from the last checkpoint (default: true). Pass false to restart from scratch. |
Example Request
curl -X POST https://api.eazip.io/jobs/550e8400-e29b-41d4-a716-446655440000/retry \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from_checkpoint": true }'Response
{
"success": true,
"job_id": "550e8400-e29b-41d4-a716-446655440000"
}Download ZIP
Download a completed ZIP. This endpoint does not require an API key — it
is protected by the signed URL embedded in download_url.
GET /download/:tokenSupports HTTP range requests (Range / If-Range headers) for partial
downloads and resumable transfers.
For auto-split jobs every zips[] entry has its own independent
download_url token. Download each ZIP individually, in parallel or
sequentially.
Error Codes
| Code | HTTP | Description |
|---|---|---|
INVALID_REQUEST | 400 | Missing or invalid parameters |
PLAN_LIMIT_EXCEEDED | 403 | Exceeded file count or retention limit for your plan |
QUOTA_EXCEEDED | 403 | Monthly stream or stored quota exceeded while overage is disabled |
NOT_FOUND | 404 | Job doesn't exist or belongs to another user |
INVALID_REQUEST | 400 | Only failed jobs can be retried |
INVALID_TOKEN | 403 | Download token is invalid |
EXPIRED_TOKEN | 403 | Download token has expired |