Upload API
Upload images in batch
Stores up to 10 images (max 50 MB combined) from repeated multipart "file" parts. Files are processed in order and results are per-file: the response is 201 when every file succeeds and 207 when results are mixed, with each item carrying either the stored upload or its own error code. When every file fails, the status reflects the per-file failures (402, 413, 415, 422, or 500) but the body is still the per-item result object, not the error envelope — request-level failures (auth, rate limit, malformed multipart, batch size or count) use the plain error envelope. Each stored file consumes one upload unit of the monthly quota.
/api/v1/upload/batchAuthentication
Send your API key as a bearer token on every request: Authorization: Bearer sw_.... Create keys in the dashboard; see the authentication docs for details.
Parameters
Request content type: multipart/form-data
| Parameter | Type | Default | Description |
|---|---|---|---|
filerequired | file (binary, repeated) | — | One part per image, all named "file". Up to 10 files and 50 MB of combined file data per request; each file also has the individual 10 MB limit and byte-signature validation of the single-upload endpoint. |
expires_in | integer | — | Lifetime of the link in seconds, from 60 (one minute) to 31,536,000 (one year). Defaults when omitted: no scheduled expiry on an active paid plan; the 30-day plan maximum on Free. Free-plan links are capped at 30 days regardless of the value sent. Applies to every file in the batch. Range: 60–31,536,000. |
Examples
curl -X POST https://shotwisp.com/api/v1/upload/batch \
-H "Authorization: Bearer $SHOTWISP_API_KEY" \
-F "file=@./first.png" \
-F "file=@./second.jpg" \
-F "expires_in=86400"const form = new FormData();
for (const file of files) form.append("file", file); // repeat the "file" part
form.append("expires_in", "86400"); // optional
const res = await fetch("https://shotwisp.com/api/v1/upload/batch", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SHOTWISP_API_KEY}` },
body: form,
});
const batch = await res.json(); // res.status: 201 all stored, 207 mixed
for (const item of batch.items) {
console.log(item.ok ? item.upload.url : `${item.source_filename}: ${item.error.code}`);
}import os, requests
res = requests.post(
"https://shotwisp.com/api/v1/upload/batch",
headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"},
files=[
("file", open("first.png", "rb")),
("file", open("second.jpg", "rb")),
],
data={"expires_in": "86400"}, # optional
)
for item in res.json()["items"]: # res.status_code: 201 all stored, 207 mixed
print(item["upload"]["url"] if item["ok"] else item["error"]["code"])Response
Success status: 201 or 207. Per-file results in request order. 201 when all files stored; 207 when some failed. Content types: application/json.
{
"items": [
{
"ok": true,
"index": 0,
"source_filename": "first.png",
"upload": {
"id": "u7Kd2mXq9RfW4bZn",
"slug": "k7mwq2ax",
"url": "https://shotwisp.com/i/k7mwq2ax",
"file_url": "https://shotwisp.com/f/k7mwq2ax",
"delete_url": "https://shotwisp.com/api/uploads/u7Kd2mXq9RfW4bZn/delete?token=Fj3kW9sLq2Xv7Rp4Tz8mNc5d",
"expires_at": "2026-08-13T14:05:00.000Z",
"size_bytes": 481290,
"content_type": "image/png",
"filename": "first.png"
}
},
{
"ok": false,
"index": 1,
"source_filename": "second.jpg",
"error": {
"code": "unsupported_type",
"message": "Unsupported image type. The file's contents do not match a supported format."
}
}
],
"summary": { "total": 2, "succeeded": 1, "failed": 1 }
}x-shotwisp-id.Errors
Every error uses the envelope {"error": {"code", "message"}}. This endpoint can return:
| Code | HTTP status | Retryable | Meaning |
|---|---|---|---|
unauthorized | 401 | no | The API key is missing, malformed, revoked, or the account is suspended or unverified. Do not retry with the same key. Fix the Authorization header or create a new API key. |
quota_exceeded | 402 | no | The monthly upload or render quota is used up and overages are off or unavailable on the plan. Do not retry until the monthly quota resets, the plan is upgraded, or overages are enabled. |
batch_too_large | 413 | no | Combined file data in a batch exceeds 50 MB. Do not retry unchanged. Split the batch or reduce combined size. |
file_too_large | 413 | no | An uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit. |
unsupported_type | 415 | no | The file's bytes do not match a supported image format (PNG, JPEG, GIF, WebP, SVG, AVIF). Do not retry unchanged. Convert the file to a supported image format. |
too_many_files | 422 | no | A batch contains more than 10 file parts. Do not retry unchanged. Send fewer files per batch. |
validation_error | 422 | no | A request field failed validation. The message states which field and why. Do not retry unchanged. The message names the failing field; fix it first. |
rate_limited | 429 | yes | The plan's per-minute request budget is spent. Rejected requests are not metered. Retry after waiting the number of seconds in the Retry-After header, then back off exponentially with jitter. |
internal | 500 | yes | Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff. |
Limits and machine-readable spec
Plan quotas, rate limits, and every fixed limit are consolidated in Limits. This endpoint is also fully described in the OpenAPI 3.1 specification (operationId createUploadBatch). Building with an AI agent? Read the AI agent guide.