Documentation menu

API reference

Uploads

Send a file to the API so that a job can use it: in one request up to 95 MB, or in parts above that.

Overview

A job needs an input. An upload is one way to supply it: you create an upload, send the file's bytes to it, and pass its id to POST /v1/jobs as input.upload. The other way is to give the job a public input.url, which needs no upload.

File sizeHow it is sentRequests
Up to 95 MB (99,614,720 bytes)In one requestPOST /v1/uploads, then one PUT
Above 95 MBIn parts of 64 MB (67,108,864 bytes)POST /v1/uploads, one PUT per part, then POST .../complete

The response to POST /v1/uploads tells you which of the two applies, in multipart, and gives you every URL to send to. All of these requests are free, and all need your API key.

Create an upload

POST/v1/uploads

Send a JSON body. A field that is not listed here is refused with bad_request.

FieldTypeDescription
size *integerThe size of the file in bytes. A whole number greater than 0, and no more than your plan's job file limit.
filenamestringThe name of the file. Optional, but give it: it helps detect the file type and names the result. Only the last path component is kept.

The response has status 201. For a file of up to 95 MB:

201 Created, one request
{
  "id": "upl_Zk3q8WcT1nRb5LxYp0Ha",
  "object": "upload",
  "size": 48211930,
  "method": "PUT",
  "headers": {
    "authorization": "Bearer <your API key>",
    "content-type": "application/octet-stream"
  },
  "expires_at": "2026-10-02T12:00:00.000Z",
  "multipart": false,
  "url": "https://api.smolmac.com/v1/uploads/upl_Zk3q8WcT1nRb5LxYp0Ha/content?filename=report.pdf"
}

For a larger file:

201 Created, in parts
{
  "id": "upl_M2xVq7TnLc0RbY5sKd9W",
  "object": "upload",
  "size": 262144000,
  "method": "PUT",
  "headers": {
    "authorization": "Bearer <your API key>",
    "content-type": "application/octet-stream"
  },
  "expires_at": "2026-10-02T12:00:00.000Z",
  "multipart": true,
  "part_size": 67108864,
  "parts": [
    { "part_number": 1, "size": 67108864, "url": "https://api.smolmac.com/v1/uploads/upl_M2xVq7TnLc0RbY5sKd9W/parts/1" },
    { "part_number": 2, "size": 67108864, "url": "https://api.smolmac.com/v1/uploads/upl_M2xVq7TnLc0RbY5sKd9W/parts/2" },
    { "part_number": 3, "size": 67108864, "url": "https://api.smolmac.com/v1/uploads/upl_M2xVq7TnLc0RbY5sKd9W/parts/3" },
    { "part_number": 4, "size": 60817408, "url": "https://api.smolmac.com/v1/uploads/upl_M2xVq7TnLc0RbY5sKd9W/parts/4" }
  ],
  "complete_url": "https://api.smolmac.com/v1/uploads/upl_M2xVq7TnLc0RbY5sKd9W/complete"
}
FieldTypeDescription
idstringThe upload id. Pass it to a job as input.upload.
objectstringAlways "upload".
sizeintegerThe size you declared.
methodstringAlways "PUT". The method for url and for each part url.
headersobjectThe headers to send with the bytes. The authorization value is a placeholder: replace it with your own key.
expires_atstringWhen the upload is deleted if no job has used it: 24 hours after it was created. Send the content and create the job before then.
multipartbooleanfalse: send the file in one request to url. true: send it in parts, then complete the upload.
urlstringOnly when multipart is false. Where to send the bytes. It carries the file name as a query parameter when you gave one.
part_sizeintegerOnly when multipart is true. The size of every part but the last: 67108864.
partsarrayOnly when multipart is true. One entry per part, in order: part_number (from 1), size (the exact number of bytes that part must carry) and url.
complete_urlstringOnly when multipart is true. Where to send the request that completes the upload.

Send the content in one request

PUT/v1/uploads/{id}/content

Send the bytes of the file as the request body, to the url from the first response.

HeaderTypeDescription
Authorization *stringBearer followed by your API key. Use a key of the same account as the first request.
Content-Length *integerThe exact number of bytes in the body. A body that is shorter or longer makes the request fail.
Content-Typestringapplication/octet-stream. The value is not used to detect the file type.
Query parameterTypeDescription
filenamestringThe name of the file. Already present in the url you were given, if you named the file when you created the upload.

The response has status 200. The upload is ready to be used by a job.

200 OK
{
  "id": "upl_Zk3q8WcT1nRb5LxYp0Ha",
  "object": "upload",
  "size": 48211930,
  "uploaded": true
}

Send the content in parts

PUT/v1/uploads/{id}/parts/{part_number}

For each entry in parts, send that slice of the file to the entry's url. Part n starts at byte (n − 1) × part_size of the file and is size bytes long. Send the same Authorization, Content-Length and Content-Type headers as for an upload in one request.

  • Content-Length must equal the part's size exactly. Any other length is refused with 400 bad_request.
  • Parts may be sent in any order.
  • Sending a part again replaces it. If a part fails, send that part again. The others are kept.
200 OK
{
  "id": "upl_M2xVq7TnLc0RbY5sKd9W",
  "object": "upload_part",
  "part_number": 1,
  "size": 67108864
}

Complete an upload in parts

POST/v1/uploads/{id}/complete

When every part has been sent, send this request with no body. It joins the parts into one file. The upload is then ready to be used by a job.

200 OK
{
  "id": "upl_M2xVq7TnLc0RbY5sKd9W",
  "object": "upload",
  "size": 262144000,
  "uploaded": true
}
  • If parts are missing, the request is refused with 409 upload_incomplete. The message lists the part numbers still to send.
  • An upload can be completed once. After that, sending a part or completing again is refused with 409 upload_already_completed.
  • A job cannot use an upload in parts until it has been completed. Until then POST /v1/jobs returns 404 not_found for it.

Rules

  • One upload, one job. A job deletes its input when it finishes, whatever the outcome. To process the same file again, upload it again.
  • Use it within 24 hours. An upload that no job has used is deleted 24 hours after it was sent. An upload in parts that is not completed within 24 hours is discarded. The deletion is made by a clean-up that runs once an hour. See How files are handled.
  • Size. The file can be as large as your plan's job file limit. See Limits.
  • Scope. An upload belongs to the account, not to the key that made it. A job can use an upload made with any key of the same account.
  • Test keys. A job made with a test key accepts an upload only if the file is one of the sample files. The check is made when the job is created. A test key cannot create an upload in parts, because no sample file is that large.
  • Sending content twice to the same upload id replaces the first content.

Errors

CodeStatusRequestWhen
bad_request400CreateThe body is not a JSON object, has an unknown field, or size is missing, not a whole number, or not above 0.
bad_request400Send content, send a partThe body is empty, or the Content-Length header is missing or 0. For a part: the part number does not exist, or the length is not the size of that part.
file_too_large413Create, send contentThe size is over the job file limit of your plan. For content sent in one request: over 95 MB. The message says which of the two limits was hit, and for the 95 MB limit it tells you to create the upload with the real size of the file, so that you are given parts.
test_mode_sample_required402CreateA test key asked for an upload above 95 MB.
not_found404Send content, send a part, completeSend content: the id in the path is not an upload id. Send a part, complete: the id is not an upload in parts of this account.
upload_incomplete409CompleteSome parts have not been sent.
upload_already_completed409Send a part, completeThe upload has already been completed.
missing_key, invalid_key, revoked_key401AllThe API key is missing, wrong or revoked.
no_payment_method, payment_required402AllA live key on an account with no plan, or with an unpaid invoice.
account_suspended, ip_not_allowed403AllThe account is suspended, or the key may not be used from this IP address.

Full example

One request

bash
# 1. Create the upload. The response contains "id" and "url".
curl -X POST "https://api.smolmac.com/v1/uploads" \
  -H "Authorization: Bearer $SMOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"report.pdf","size":48211930}'

# 2. Send the bytes to the url from step 1. curl sets Content-Length itself.
curl -X PUT "https://api.smolmac.com/v1/uploads/upl_Zk3q8WcT1nRb5LxYp0Ha/content?filename=report.pdf" \
  -H "Authorization: Bearer $SMOL_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @report.pdf

# 3. Start a job with the upload id.
curl -X POST "https://api.smolmac.com/v1/jobs" \
  -H "Authorization: Bearer $SMOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"operation":"compress","input":{"upload":"upl_Zk3q8WcT1nRb5LxYp0Ha"}}'

Any size

This code handles both cases. It reads multipart from the response and sends the file in one request or in parts. The SDKs have the same logic built in: smol.uploads.upload() in TypeScript and smol.upload() in Python.

import { open, stat } from "node:fs/promises";

const api = "https://api.smolmac.com";
const auth = { Authorization: `Bearer ${process.env.SMOL_API_KEY}` };
const path = "talk.mov";

async function call(url, init) {
  const response = await fetch(url, init);
  const body = await response.json();
  if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}

// fetch sets Content-Length from the buffer.
const put = (url, bytes) =>
  call(url, { method: "PUT", headers: { ...auth, "Content-Type": "application/octet-stream" }, body: bytes });

// 1. Create the upload.
const { size } = await stat(path);
const upload = await call(`${api}/v1/uploads`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ filename: path, size }),
});

// 2. Send the bytes: in one request, or part by part.
const file = await open(path);
try {
  if (!upload.multipart) {
    await put(upload.url, await file.readFile());
  } else {
    for (const part of upload.parts) {
      const bytes = Buffer.alloc(part.size);
      await file.read(bytes, 0, part.size, (part.part_number - 1) * upload.part_size);
      await put(part.url, bytes);
    }
    await call(upload.complete_url, { method: "POST", headers: auth });
  }
} finally {
  await file.close();
}

// 3. Start a job with the upload id.
const job = await call(`${api}/v1/jobs`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ operation: "compress", input: { upload: upload.id } }),
});
console.log(job.id, job.status);