Documentation menu

API reference

Jobs

Create, read, list, cancel and download asynchronous jobs, and fetch their signed receipts.

Overview

A job does the same work as a direct request, but in the background. Use one for a file over 25 MB, for any video, and for any Word document, spreadsheet or presentation. You create the job, then wait for it to finish by polling or by webhook, then download the result.

MethodPathPurpose
POST/v1/jobsCreate a job
GET/v1/jobs/{id}Retrieve a job
GET/v1/jobsList jobs
DELETE/v1/jobs/{id}Cancel a job and delete its files
GET/v1/jobs/{id}/outputDownload the output
GET/v1/jobs/{id}/receiptGet the signed receipt
GET/v1/receipt_keysGet the public keys that verify receipts

A job is visible only to keys of the account that created it, and only to keys of the same mode. A test key cannot see a job made with a live key.

Create a job

POST/v1/jobs

Send a JSON body of at most 64 KB. A field that is not listed here is refused with bad_request, and param names it.

HeaderTypeDescription
Authorization *stringBearer followed by your API key.
Content-Typestringapplication/json.
Idempotency-Keystring1 to 255 characters. Makes a retry return the first response without creating a second job. Remembered for 24 hours. See Idempotency.
FieldTypeDescription
operation *stringcompress, convert or strip_metadata.
input *objectWhere the file comes from. It must have exactly one of upload and url.
input.uploadstringThe id of an upload whose content has been sent, and which has been completed if it was sent in parts. If no content is found for the id, the request is refused with 404 not_found.
input.urlstringA public https URL (see URL rules). The file is fetched with a GET when the job starts. The response must carry a Content-Length header, and the file must be within your plan's job file limit. If the fetch fails, the job is still created, and then fails with input_fetch_failed. Not available with a test key.
input.filenamestringThe name of the file. Default: the name given to the upload, or the last path segment of the URL. Used to detect the file type and to name the output.
optionsobjectHow to process the file. See Options below. For convert, options.convert.to is required.
presetstringThe name or id of a preset. Its options are applied first, and the fields in options override them one by one. A preset the account does not have is refused with 400 bad_request.
outputobjectSend the result to your own storage and do not keep it with us. See Output to your storage.
output.urlstringRequired when output is present. A public https URL. The result is sent to it with a PUT that carries Content-Type and Content-Length. Redirects are not followed. Any response outside 200 to 299 fails the job with output_upload_failed.
output.headersobjectHeaders to send with that PUT. Up to 20 entries. Each value is a string of at most 2,000 characters, and each name is at most 100 characters.
retention_secondsintegerHow long the output is kept after the job finishes. A whole number from 60 to 86400. Default: your account's setting, which is 3600 unless you changed it.
webhook_urlstring or nullA public https URL that receives this job's event, in addition to the account's webhook endpoints. The delivery is signed with the account's signing secret.
metadataobjectYour own labels. Up to 20 entries. Each value is a string of at most 500 characters, and each key is at most 40 characters. Returned unchanged on the job and in webhooks.

Options

options has one section per kind of file. Only the section for the kind you sent is used. This object shows every option with its default:

options, with defaults
{
  "image": {
    "format": "original",
    "quality": 75,
    "resize": { "mode": "fit", "width": 2000, "height": 2000 },
    "strip_metadata": true
  },
  "pdf": { "quality": "medium" },
  "video": {
    "format": "mp4",
    "codec": "h265",
    "quality": "high",
    "resolution": "original",
    "strip_audio": false,
    "slow": false,
    "crf": null,
    "preset": null,
    "audio_quality": "high"
  },
  "audio": {
    "format": "aac",
    "quality": "high",
    "channels": "stereo",
    "strip_metadata": false,
    "bitrate_kbps": null,
    "sample_rate_hz": null
  },
  "convert": { "to": null, "page": 1, "dpi": 144 }
}
  • image.resize may be false to keep the image's dimensions.
  • Only the five fields shown as null accept null, which means "use the default".
  • An unknown section or field, or a value that is not allowed, is refused with invalid_options.
  • The image, PDF, audio and convert options are described on the compress and convert pages. Every option is listed in Options.
Video optionTypeDescription
video.formatstringmp4 (default), mov, webm, mkv.
video.codecstringh265 (default), h264, vp9. WebM output always uses VP9.
video.qualitystringtiny, low, balanced, high (default), maximum, web. See video tiers.
video.resolutionstringoriginal (default), 3840, 1920, 1280 or 854. The number is a maximum width; the matching maximum heights are 2160, 1080, 720 and 480. The video is only ever scaled down.
video.strip_audiobooleanDefault false. true removes the audio track.
video.slowbooleanDefault false. true uses the slow encoder preset: a smaller file for more time.
video.crfinteger or null0 to 63. Replaces the CRF of the quality tier.
video.presetstring or nullultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow. Replaces the encoder preset.
video.audio_qualitystringtiny, low, balanced, high (default), maximum. The tier of the audio track.

URL rules

input.url, output.url and webhook_url must each be a public https URL. That means:

  • The scheme is https and the port is the default, 443.
  • There is no user name or password in the URL.
  • The host is a public host name. localhost, single-label names, and names ending in .local, .internal, .lan, .home, .corp, .test, .invalid, .onion or .localhost are refused.
  • An IPv6 address is refused. An IPv4 address is accepted only if it is public.

A URL that breaks a rule is refused with 400 bad_request. When an input is fetched, up to three redirects are followed, and each hop must meet the same rules.

Response

Status 202 and the job object, with status: "queued". A response replayed for an idempotency key also carries Smol-Idempotent-Replay: true. A repeat that arrives while the first request is still being handled gets 409 idempotency_in_progress with Retry-After: 1.

202 Accepted
{
  "id": "job_7hQ2mVx9KdL4sTn0BwYe",
  "object": "job",
  "livemode": true,
  "operation": "compress",
  "status": "queued",
  "created_at": "2026-10-01T12:00:03.118Z",
  "started_at": null,
  "completed_at": null,
  "progress": { "percent": 0 },
  "input": { "filename": "report.pdf", "size": 48211930 },
  "result": null,
  "error": null,
  "billed": false,
  "expires_at": null,
  "files_deleted": false,
  "metadata": { "order": "1234" }
}

Example

curl -X POST "https://api.smolmac.com/v1/jobs" \
  -H "Authorization: Bearer $SMOL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2f0e-8f0b-4c53-9d3a-2b7c1c0f5a11" \
  -d '{
    "operation": "compress",
    "input": { "upload": "upl_Zk3q8WcT1nRb5LxYp0Ha" },
    "options": { "pdf": { "quality": "small" } },
    "metadata": { "order": "1234" }
  }'

The job object

a job that succeeded
{
  "id": "job_7hQ2mVx9KdL4sTn0BwYe",
  "object": "job",
  "livemode": true,
  "operation": "compress",
  "status": "succeeded",
  "created_at": "2026-10-01T12:00:03.118Z",
  "started_at": "2026-10-01T12:00:04.402Z",
  "completed_at": "2026-10-01T12:00:21.950Z",
  "progress": { "percent": 100 },
  "input": { "filename": "report.pdf", "size": 48211930 },
  "result": {
    "kind": "pdf",
    "input_format": "pdf",
    "output_format": "pdf",
    "original_size": 48211930,
    "output_size": 9310224,
    "savings_percent": 80.7,
    "kept_original": false,
    "increased": false,
    "width": null,
    "height": null,
    "pages": 212,
    "duration_seconds": null,
    "video_codec": null,
    "warnings": [],
    "download_url": "https://api.smolmac.com/v1/jobs/job_7hQ2mVx9KdL4sTn0BwYe/output?expires=1790859621&signature=5f0c2a9e7b3d4c1f8a6e0d2b4c6f8a1e3d5c7b9a0f2e4d6c8b0a1f3e5d7c9b2a",
    "download_expires_at": "2026-10-01T13:00:21.950Z"
  },
  "error": null,
  "billed": true,
  "expires_at": "2026-10-01T13:00:21.950Z",
  "files_deleted": false,
  "metadata": { "order": "1234" }
}
FieldTypeDescription
idstringThe job id: job_ followed by 20 letters and digits.
objectstringAlways "job".
livemodebooleantrue for a job made with a live key, false for a test key.
operationstringcompress, convert or strip_metadata.
statusstringqueued, processing, succeeded, failed or canceled. See Status lifecycle.
created_atstringWhen the job was created.
started_atstring or nullWhen an engine first accepted the job. null while the job is queued.
completed_atstring or nullWhen the job reached succeeded, failed or canceled. null before that.
progress.percentinteger0 while queued, 50 while processing, 100 once finished. It is a coarse indicator, not a measure of the work done.
input.filenamestring or nullThe name of the input file, or null if none was given.
input.sizeinteger or nullThe size of the input in bytes. null for a URL input that has not been fetched yet.
resultobject or nullPresent when the job succeeded: the result object plus the two fields below. null otherwise.
result.download_urlstring or nullA signed link to the output. It needs no API key. null when the output went to your own storage, and once the output has been deleted.
result.download_expires_atstring or nullWhen the link stops working. The same time as expires_at. null whenever download_url is null.
errorobject or nullPresent when the job failed or was canceled: type, code and message. The codes are listed in Errors. null otherwise.
billedbooleantrue when the job was charged. false until the job finishes, and for failed, canceled, kept-original and test-mode jobs.
expires_atstring or nullWhen the output is deleted: completed_at plus the retention period. null until the job finishes.
files_deletedbooleantrue when the input has been deleted and there is no stored output left.
metadataobjectThe metadata you sent, or an empty object.

Timestamps are ISO 8601 strings in UTC with milliseconds. Sizes are in bytes.

Status lifecycle

StatusMeaningFinal
queuedAccepted. A URL input is being fetched, or the job is waiting for a free engine.No
processingAn engine is working on the file.No
succeededThe work is done. result is set. This includes a job that returned the input unchanged, with kept_original: true.Yes
failedThe work could not be done. error says why.Yes
canceledYou sent DELETE before the job finished. error.code is canceled.Yes
  • The usual path is queued, processing, then one of the final statuses. A final status never changes.
  • A job can go from processing back to queued if the engine working on it restarts. It is started again on another engine. After three attempts it fails with internal.
  • A job that waits more than 30 minutes for a free engine fails with timeout. A job that is processed for more than 4 hours fails with timeout.
  • When a job reaches a final status, its input is deleted, billed is set, and a webhook event is sent.
  • The job record is kept for 30 days after the job finishes. After that, retrieving the job returns 404. Its entry in the list is removed 30 days after the job was created.

Retrieve a job

GET/v1/jobs/{id}

Returns status 200 and the job object. Returns 404 not_found if there is no such job, if it belongs to another account, if it was made with a key of the other mode, or if its record has been deleted.

bash
curl "https://api.smolmac.com/v1/jobs/job_7hQ2mVx9KdL4sTn0BwYe" \
  -H "Authorization: Bearer $SMOL_API_KEY"

List jobs

GET/v1/jobs

Returns the account's jobs, newest first. A live key lists live jobs. A test key lists test jobs. The list holds the jobs created in the last 30 days.

Query parameterTypeDescription
limitintegerHow many jobs to return. 1 to 100. Default 20. A larger number is treated as 100.
starting_afterstringA job id. The list continues with the jobs created before that one. An id that is not a job on your account is refused with 400 bad_request.
statusstringReturn only jobs with this status: queued, processing, succeeded, failed or canceled.
200 OK
{
  "object": "list",
  "data": [
    {
      "id": "job_7hQ2mVx9KdL4sTn0BwYe",
      "object": "job",
      "livemode": true,
      "operation": "compress",
      "status": "succeeded",
      "kind": "pdf",
      "error_code": null,
      "billed": true,
      "created_at": "2026-10-01T12:00:03.118Z",
      "completed_at": "2026-10-01T12:00:21.950Z"
    },
    {
      "id": "job_Lp3Xc8VbN1mQw6Zt0RdK",
      "object": "job",
      "livemode": true,
      "operation": "convert",
      "status": "failed",
      "kind": null,
      "error_code": "unsupported_target",
      "billed": false,
      "created_at": "2026-10-01T11:42:50.007Z",
      "completed_at": "2026-10-01T11:42:53.611Z"
    }
  ],
  "has_more": true
}

Each entry is a summary, not the full job object. It has no file name, no result and no download link. Fetch the job by id for those.

Summary fieldTypeDescription
idstringThe job id.
objectstringAlways "job".
livemodebooleantrue for a live job.
operationstringcompress, convert or strip_metadata.
statusstringThe status of the job.
kindstring or nullThe kind of file, once the job has succeeded: image, pdf, video, audio, word, sheet, presentation, table or font.
error_codestring or nullThe error code of a failed or canceled job.
billedbooleantrue when the job was charged.
created_atstringWhen the job was created.
completed_atstring or nullWhen the job finished.

To page through the list, pass the id of the last entry as starting_after, and repeat while has_more is true.

bash
curl "https://api.smolmac.com/v1/jobs?limit=50&status=failed&starting_after=job_Lp3Xc8VbN1mQw6Zt0RdK" \
  -H "Authorization: Bearer $SMOL_API_KEY"

Cancel or delete a job

DELETE/v1/jobs/{id}

What it does depends on the job's status.

  • Queued or processing. The work is stopped. The status becomes canceled, with error.code: "canceled". A job.canceled event is sent. The job is not billed.
  • Succeeded, failed or canceled. The status does not change, and no event is sent.
  • In every case the input and the stored output are deleted at once, without waiting for expires_at. The download link stops working.
200 OK
{
  "id": "job_7hQ2mVx9KdL4sTn0BwYe",
  "object": "job",
  "deleted": true
}

deleted refers to the files. The job record stays, so you can still retrieve the job, which now shows files_deleted: true, and fetch its receipt. A file already sent to your own output.url is not affected. Returns 404 not_found under the same conditions as retrieving a job.

bash
curl -X DELETE "https://api.smolmac.com/v1/jobs/job_7hQ2mVx9KdL4sTn0BwYe" \
  -H "Authorization: Bearer $SMOL_API_KEY"

Download the output

GET/v1/jobs/{id}/output

Returns the output file of a job that succeeded. There are two ways to authorise the request.

WayHowUse it when
Signed linkRequest result.download_url exactly as given. It carries the query parameters expires (a Unix time in seconds) and signature. Send no API key.You hand the link to a browser or to another service.
API keyRequest the path without query parameters, with your Authorization header. The key must be of the job's account and mode.Your own server downloads the file.

The response has status 200 and the file as its body.

Response headerTypeDescription
Content-TypestringThe media type of the output.
Content-LengthintegerThe size of the output in bytes.
Content-Dispositionstringattachment, with the input file name and the extension of the output format.
Cache-ControlstringAlways private, no-store.
  • A signature that does not match, or a link past its expires time, returns 403 invalid_signature.
  • 404 not_found is returned when the job has not succeeded, when the output has been deleted, and when the output was sent to your own storage.
  • Anyone who has the signed link can download the file until it expires. Treat the link as a secret.
bash
# With the signed link from result.download_url
curl -o report.min.pdf "https://api.smolmac.com/v1/jobs/job_7hQ2mVx9KdL4sTn0BwYe/output?expires=1790859621&signature=5f0c2a9e7b3d4c1f8a6e0d2b4c6f8a1e3d5c7b9a0f2e4d6c8b0a1f3e5d7c9b2a"

# With the API key
curl -o report.min.pdf "https://api.smolmac.com/v1/jobs/job_7hQ2mVx9KdL4sTn0BwYe/output" \
  -H "Authorization: Bearer $SMOL_API_KEY"

Get the receipt

GET/v1/jobs/{id}/receipt

Returns a signed statement of what was processed and when the files were deleted. It is available once the job has a final status. Before that the request is refused with 409 job_not_finished. See Receipts for how to verify one.

200 OK
{
  "object": "receipt",
  "payload": "{\"version\":1,\"job_id\":\"job_7hQ2mVx9KdL4sTn0BwYe\",\"operation\":\"compress\",\"status\":\"succeeded\",\"standard\":\"v1\",\"input_sha256\":\"3b1f0c9a7d52e84f6a0b9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5\",\"input_size\":48211930,\"output_sha256\":\"c41d7e0b95a2f3861b7c4d9e0f2a3b5c6d8e7f90a1b2c3d4e5f60718293a4b6c\",\"output_size\":9310224,\"created_at\":\"2026-10-01T12:00:03.118Z\",\"completed_at\":\"2026-10-01T12:00:21.950Z\",\"input_deleted_at\":\"2026-10-01T12:00:21.958Z\",\"output_deleted_at\":\"2026-10-01T13:00:22.104Z\",\"output_delivered_to_customer_storage\":false,\"issued_at\":\"2026-10-01T14:12:09.331Z\"}",
  "signature": "kX9v2mQpL7sT4wZ1aB8cD3eF6gH0jK5nR2uY7xV4bN1mC8qW3eR6tY9uI0oP5aS2dF7gH4jK1lZ8xC3vB6nM0Q==",
  "algorithm": "Ed25519",
  "key_id": "smol-receipt-1"
}
FieldTypeDescription
objectstringAlways "receipt".
payloadstringThe statement, as a JSON document inside a string. The signature covers this string exactly, byte for byte, so verify it before you parse it.
signaturestring or nullThe Ed25519 signature of the UTF-8 bytes of payload, in base64. null if the receipt could not be signed.
algorithmstringAlways "Ed25519".
key_idstring or nullIdentifies the key that signed the receipt.

The payload, once parsed, has these fields:

Payload fieldTypeDescription
versionintegerThe version of the receipt format. 1.
job_idstringThe job this receipt is for.
operationstringcompress, convert or strip_metadata.
statusstringsucceeded, failed or canceled.
standardstringThe version of the compression standard that was applied. "v1".
input_sha256string or nullThe SHA-256 of the input, in hex. null if the job did not succeed.
input_sizeinteger or nullThe size of the input in bytes.
output_sha256string or nullThe SHA-256 of the output, in hex. null if the job did not succeed.
output_sizeinteger or nullThe size of the output in bytes. null if the job did not succeed.
created_atstringWhen the job was created.
completed_atstringWhen the job finished.
input_deleted_atstring or nullWhen the input was deleted from our storage.
output_deleted_atstring or nullWhen the output was deleted from our storage. null while it is still stored, and when it was never stored.
output_delivered_to_customer_storagebooleantrue when the job had an output.url.
issued_atstringWhen this receipt was produced. A new receipt is produced on every request.

To get a receipt that records the deletion of the output, request it after expires_at or after a DELETE. Receipts can be fetched for 30 days after the job finishes.

Receipt keys

GET/v1/receipt_keys

Returns the public keys that receipts are signed with. It needs no API key. To verify a receipt, find the entry whose key_id equals the receipt's, and check the receipt's signature against the UTF-8 bytes of its payload with that public key.

200 OK
{
  "object": "list",
  "data": [
    {
      "key_id": "smol-receipt-1",
      "algorithm": "Ed25519",
      "format": "spki-base64",
      "public_key": "MCowBQYDK2VwAyEAq8Jm2Yx0v5cT1nRb7LpKd3WzHs9QeFa4UgXo6ViNtB0=",
      "status": "active",
      "retired_at": null
    }
  ],
  "has_more": false
}
FieldTypeDescription
key_idstring or nullThe identifier that appears as key_id in a receipt.
algorithmstringAlways "Ed25519".
formatstringAlways "spki-base64": the public key is a DER SubjectPublicKeyInfo structure, encoded in base64.
public_keystringThe public key, in that format.
statusstringactive for the key that signs new receipts. retired for a key that no longer signs. Retired keys stay listed so that old receipts can still be verified.
retired_atstring or nullWhen a retired key stopped signing. null for the active key.

The response carries Cache-Control: public, max-age=300. Fetch the keys over HTTPS from api.smolmac.com and store them. Do not take a key from the same place a receipt came from, if that place is not us.

Errors

These are the errors the job endpoints return as HTTP responses. A failure of the work itself is reported on the job, in error. Those codes are listed under job errors.

CodeStatusEndpointWhen
bad_request400Create, listThe body is not valid JSON or is over 64 KB. A field is unknown, missing or has a value that is not allowed. A URL breaks the URL rules. The preset named does not exist. The Idempotency-Key is empty or over 255 characters. On list: status or starting_after is not valid.
invalid_options400CreateAn option is unknown or has a value that is not allowed. A convert job has no options.convert.to.
missing_key, invalid_key, revoked_key401AllThe API key is missing, wrong or revoked. Not checked for a download with a signed link.
no_payment_method, payment_required402AllA live key on an account with no plan, or with an unpaid invoice.
spend_cap_reached402CreateThe account has reached its monthly spend cap.
test_mode_sample_required402CreateA test key named an upload that is not a published sample file, or that is over 25 MB, or used input.url.
account_suspended403AllThe account is suspended.
ip_not_allowed403AllThe key may not be used from this IP address.
feature_not_enabled403CreateThe options ask for HEIC output by name, and it is not enabled for the account.
invalid_signature403OutputThe signed link is wrong or has expired.
not_found404AllThe job, its output, or the upload named in input.upload does not exist for this account and mode.
idempotency_conflict409CreateThe Idempotency-Key was already used with a different body.
idempotency_in_progress409CreateThe first request with this Idempotency-Key is still being handled. Retry-After is 1.
job_not_finished409ReceiptThe job has no final status yet.
concurrency_limited429CreateThe account already has its limit of queued and processing jobs. Retry-After is 5.
internal500AllSomething went wrong on our side.