Unzip archive
Send a .zip. Get back one link per file inside it, with the archive’s folder paths kept.
SyncPOST /v1/utilities/unzip-filesWaits up to 90 s for the finished job.
AsyncPOST /v1/utilities/unzip-files/jobsAnswers 202 with the job id at once.
What it’s for
- Uploads that arrive zippedUnpack what a customer or partner sent, then handle each file on its own.
- Bulk importsA folder of invoices sent as one archive becomes a list of files you can loop over.
-
Folders that mean somethingEvery file keeps its path, like
docs/invoice.pdf, so structure survives. - Archives you didn’t makeEntry paths are normalised, so a crafted archive can’t write outside its job’s folder.
How it runs
-
DownloadReads the .zip from your
http(s)://ors3://URL into memory. - WalkGoes through the entries one at a time. Directory entries are skipped.
-
NormalisePaths with
.., leading slashes or backslashes are cleaned, so every file stays inside the job’s folder. -
StoreOne file per entry, each with its
path, plusfileCount.
There is no file-count limit, but thousands of small files take a while. Use the
async route for anything big; the sync route answers 202 with the job
id after 90 s anyway.
Input
Send these fields as input in the request body. Anything not listed is
refused with 400.
| Field | Type | Required or optional | Accepts |
|---|---|---|---|
url |
string |
Required |
http://, https:// or s3:// URL, 1–4,096
characters. The .zip to extract.
|
The request body
| Field | Required or optional | What it is |
|---|---|---|
input |
Required | The fields above. |
metadata |
Optional | Your own key–value pairs, returned on the job. At most 50 keys, 4 KB serialised. |
callbackUrl |
Optional | Async route only. The finished job is POSTed here, signed. Up to 2,048 characters. |
What comes back
| Field | Value |
|---|---|
files[].url
|
A link to the file. |
result.fileCount
|
Number of files extracted, equal to files.length. |
files[].path
|
The entry’s path inside the archive, e.g. docs/invoice.pdf.
|
files[].name
|
The last path segment, e.g. invoice.pdf. |
files[].contentType
|
From the file extension; application/octet-stream when it isn’t
known.
|
files[].size
|
The uncompressed size in bytes. |
Example
curl https://api.servicelabs.dev/v1/utilities/unzip-files \
-H "Authorization: Bearer $DU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"url": "https://example.com/bundle.zip"
}
}'
# 1. Start the job. Answers 202 straight away.
curl https://api.servicelabs.dev/v1/utilities/unzip-files/jobs \
-H "Authorization: Bearer $DU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unzip-files-0001" \
-d '{
"input": {
"url": "https://example.com/bundle.zip"
},
"callbackUrl": "https://example.com/hooks/servicelabs"
}'
# 2. Collect it by id. Long-polls for up to 60 s.
curl "https://api.servicelabs.dev/v1/jobs/job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3?wait=60" \
-H "Authorization: Bearer $DU_API_KEY"
const API = 'https://api.servicelabs.dev/v1'
const headers = {
Authorization: `Bearer ${process.env.DU_API_KEY}`,
'Content-Type': 'application/json',
}
// Sync: holds the connection for up to 90 s
const res = await fetch(`${API}/utilities/unzip-files`, {
method: 'POST',
headers,
body: JSON.stringify({ input: { url: 'https://example.com/bundle.zip' } }),
})
if (res.status === 400) throw new Error((await res.json()).error.message)
let job = await res.json()
// Still running after 90 s? It carries on. Collect it by id.
while (job.status === 'queued' || job.status === 'running') {
job = await (await fetch(`${API}/jobs/${job.id}?wait=60`, { headers })).json()
}
if (job.status === 'succeeded') console.log(job.result.files.map((f) => f.url))
else console.error(job.error.code, job.error.message)
{
"id": "job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3",
"object": "job",
"utility": "unzip-files",
"mode": "sync",
"status": "succeeded",
"createdAt": "2026-09-10T17:30:06.650Z",
"finishedAt": "2026-09-10T17:30:09.009Z",
"timings": { "queueMs": 0, "upstreamMs": 2140, "storageMs": 196, "totalMs": 2359 },
"result": {
"files": [
{
"url": "https://…/invoices/jan.pdf",
"name": "jan.pdf",
"path": "invoices/jan.pdf",
"size": 120334,
"contentType": "application/pdf"
},
{
"url": "https://…/invoices/feb.pdf",
"name": "feb.pdf",
"path": "invoices/feb.pdf",
"size": 118902,
"contentType": "application/pdf"
},
{
"url": "https://…/summary.csv",
"name": "summary.csv",
"path": "summary.csv",
"size": 2211,
"contentType": "text/csv"
}
],
"fileCount": 3
},
"error": null,
"links": {
"self": "/v1/jobs/job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3"
}
}
An example job. File links point at the stored output; download anything you need to keep.
{
"error": {
"code": "invalid_request",
"message": "input failed validation for unzip-files",
"details": [
{
"path": "input.url",
"message": "must be an http://, https:// or s3:// URL"
}
]
}
}
Input is checked before anything runs. A 400 creates no job and costs nothing.
Limits
| Limit | Value |
|---|---|
| Runs up to | 900 s (15 min) |
| Files per archive | No limit |
| Archive size | Read into memory; extremely large archives can run out |
| Directory entries | Skipped |
| An empty archive | Succeeds with fileCount: 0 |
| Sync wait | 90 s, then 202 with the job id |
| Long-poll | Up to 60 s per GET /v1/jobs/{id}?wait= |
| Rate limit | Per key; 600 requests a minute unless set otherwise |
| Output files | Kept for a limited period; download what you need |
When it fails
A 400 means nothing ran. Everything else is a failed job with
status: "failed", an error code and the utility’s own message.
| Situation | Status | Code | Job |
|---|---|---|---|
url missing, or not http(s):// or s3://
|
400 | invalid_request |
No job |
| The archive can’t be downloaded, or isn’t a valid zip | 502 | upstream_error |
Failed job |
| Out of memory on a huge archive | 502 | upstream_error |
Failed job |
| Ran past 900 s | 504 | upstream_timeout |
Failed job |
Any call can also get 401 unauthorized, 403 forbidden (the
key isn’t allowed this utility) or 429 rate_limited. The full list is in
Conventions & errors.
Reference
- Unzip archive guideEvery option, with worked examples.
-
runUnzipFiles
POST /v1/utilities/unzip-files -
createUnzipFilesJob
POST /v1/utilities/unzip-files/jobs -
getJob
GET /v1/jobs/{id} - JobsStatuses, timings, retries and cancelling.
- CallbacksVerifying the signature on a finished job.
Also in service
Try Unzip archive on your own file.
The console’s playground runs unzip-files against any URL you give it and shows the job it returns.