Skip to content
Image KB Reducer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Batch form uploads

Post a batch of images to the HTTP API from curl, Python, and Ruby, reading X-Result-Bytes and handling target-unreachable.

The same POST /v1/compress call from three languages, compressing a directory of images and reporting what happened to each one — including the one honest failure this endpoint can hand back, target-unreachable.

Every sample globs in/*.jpg. Widen that to PNG and WebP freely — but not to HEIC or AVIF: this endpoint accepts JPEG, PNG, and WebP only and refuses the other two with 415 unsupported-format, so a directory straight off a phone needs converting before any of this runs. See Limits for why.

curl

#!/usr/bin/env bash
set -euo pipefail

API_KEY="${IKB_API_KEY:?set IKB_API_KEY}"
TARGET="50kb"

mkdir -p out

for file in in/*.jpg; do
  out="out/$(basename "$file")"
  response=$(curl -sS -w '\n%{http_code}' \
    "https://api.imagekbreducer.com/v1/compress" \
    -H "Authorization: Bearer $API_KEY" \
    -F "file=@$file" \
    -F "target=$TARGET" \
    -D - \
    -o "$out")
  status=$(tail -n1 <<<"$response")

  if [ "$status" = "200" ]; then
    result_bytes=$(grep -i '^x-result-bytes:' <<<"$response" | tr -d '\r' | cut -d' ' -f2)
    echo "$file -> $out ($result_bytes bytes)"
  else
    echo "$file: failed with status $status" >&2
    rm -f "$out"
  fi
done

-D - prints response headers to stdout alongside the body’s status line, which is how this extracts X-Result-Bytes without a second request. For the error body’s JSON shape on a non-200 (400/401/413/415/422/…), pipe the same response through jq instead of writing straight to a file — see the Python example below for that shape read directly.

Python (requests)

import os
import sys
from pathlib import Path

import requests

API_KEY = os.environ["IKB_API_KEY"]
TARGET = "50kb"
API_URL = "https://api.imagekbreducer.com/v1/compress"


def compress_one(path: Path, out_dir: Path) -> None:
    with path.open("rb") as f:
        response = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": (path.name, f)},
            data={"target": TARGET},
            timeout=90,  # see /api/limits -- WALL_CLOCK_MS is not a reliable server-side bound
        )

    if response.status_code == 200:
        out_path = out_dir / path.name
        out_path.write_bytes(response.content)
        print(f"{path} -> {out_path} ({response.headers['X-Result-Bytes']} bytes, "
              f"{response.headers['X-Attempts']} attempts)")
        return

    body = response.json()
    if body.get("error") == "target-unreachable":
        # An answer, not a failure: body["closest"] is the smallest size the
        # search actually reached, and body["suggestions"] names the levers
        # that would make `target` reachable. See /api/errors.
        print(f"{path}: could not reach {TARGET} -- closest was {body['closest']} bytes "
              f"({body['reason']})", file=sys.stderr)
        for s in body.get("suggestions", []):
            print(f"  try: {s}", file=sys.stderr)
    else:
        print(f"{path}: {response.status_code} {body.get('error')} -- {body.get('message')}",
              file=sys.stderr)


def main() -> None:
    in_dir, out_dir = Path("in"), Path("out")
    out_dir.mkdir(exist_ok=True)
    for path in sorted(in_dir.glob("*.jpg")):
        compress_one(path, out_dir)


if __name__ == "__main__":
    main()

Ruby (Net::HTTP)

require "net/http"
require "uri"
require "json"

API_KEY = ENV.fetch("IKB_API_KEY")
TARGET = "50kb"
API_URL = URI("https://api.imagekbreducer.com/v1/compress")

def compress_one(path, out_dir)
  request = Net::HTTP::Post.new(API_URL)
  request["Authorization"] = "Bearer #{API_KEY}"
  file = File.open(path)
  begin
    request.set_form([["file", file, { filename: File.basename(path) }], ["target", TARGET]], "multipart/form-data")

    # read_timeout defaults to 60s, and /api/limits records a successful
    # response that took 79 -- WALL_CLOCK_MS is not a reliable server-side bound.
    response = Net::HTTP.start(API_URL.host, API_URL.port, use_ssl: true, read_timeout: 90) do |http|
      http.request(request)
    end
  ensure
    file.close
  end

  case response
  when Net::HTTPOK
    out_path = File.join(out_dir, File.basename(path))
    File.binwrite(out_path, response.body)
    puts "#{path} -> #{out_path} (#{response["X-Result-Bytes"]} bytes, " \
         "#{response["X-Attempts"]} attempts)"
  else
    body = JSON.parse(response.body)
    if body["error"] == "target-unreachable"
      # An answer, not a failure -- see /api/errors.
      warn "#{path}: could not reach #{TARGET} -- closest was #{body["closest"]} bytes " \
           "(#{body["reason"]})"
      body.fetch("suggestions", []).each { |s| warn "  try: #{s}" }
    else
      warn "#{path}: #{response.code} #{body["error"]} -- #{body["message"]}"
    end
  end
end

Dir.mkdir("out") unless Dir.exist?("out")
Dir.glob("in/*.jpg").sort.each { |path| compress_one(path, "out") }

Handling target-unreachable

All three examples above treat 422 target-unreachable as a distinct case from every other failure, because it is one: the response carries closest (the smallest size the search actually reached) and suggestions (the specific levers — switch format, raise the target, allow downscaling — that would make target reachable). A batch job that just logs every non-200 as “failed” throws that information away. See Errors for the full shape and Give-up codes for what each reason means.

Concurrency

Nothing above sends requests concurrently — each loop is sequential, one file at a time. If you parallelise this yourself, keep each key’s request rate under the 60-per-60-seconds limit (see Authentication) and read Limits before assuming a slow request will time out on its own; it may not.

Was this page helpful?