npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tinify-dev/client

v0.1.3

Published

Zero-dependency TypeScript client for the Tinify.dev image API: compress, resize, crop, convert, batches, and usage.

Readme

@tinify-dev/client

Zero-dependency TypeScript client for the Tinify.dev image API: compress, resize, crop, convert, durable batches, and account usage.

  • Zero runtime dependencies — built on the platform fetch, FormData, and Blob.
  • Node.js >= 20, dual ESM + CJS, fully typed.
  • Honest results — compression never returns more bytes than you sent; when the API cannot shrink a file it says so via optimized: false. (Resize, crop, and conversion report byte changes plainly — conversion can legitimately grow a file.)
  • Safe by default — automatic idempotency keys, retries with Retry-After support, typed error codes with request_id for support.

Not affiliated with TinyPNG's tinify package. This client talks to the Tinify.dev API.

Install

npm install @tinify-dev/client

Quickstart

import { TinifyClient } from "@tinify-dev/client";

const client = new TinifyClient({ apiKey: process.env.TINIFY_API_KEY });

const result = await client.compress("./photo.png", { quality_mode: "balanced" });
console.log(result.data.original_bytes, "->", result.data.result_bytes);

if (result.data.optimized === false) {
  console.log("Already as small as it gets — the original bytes were returned.");
}

const blob = await client.download(result);
await fs.writeFile("./photo.min.png", new Uint8Array(await blob.arrayBuffer()));

CJS works too:

const { TinifyClient } = require("@tinify-dev/client");

Authentication

Every call needs an API key (tnf_live_* for production, tnf_test_* for development), created at tinify.dev/developers. Pass it as apiKey or set the TINIFY_API_KEY environment variable. Keys are sent as Authorization: Bearer <key>.

new TinifyClient({
  apiKey: "tnf_live_...",        // default: process.env.TINIFY_API_KEY
  baseUrl: "https://api.tinify.dev", // default: process.env.TINIFY_BASE_URL or this value
  maxRetries: 3,                  // retries after the first attempt (429/503/network)
  timeoutMs: 60_000,              // per-attempt timeout
  fetch: customFetch,             // injectable, e.g. for proxies or tests
});

Inputs

All image methods accept a file path (Node.js only), Buffer, Uint8Array, ArrayBuffer, Blob, or File.

Methods

| Method | Endpoint | Notes | | --- | --- | --- | | compress(input, { quality_mode?, target_size_bytes? }) | POST /api/v1/images/compress | quality_mode: balanced (default), best_quality, lossless. target_size_bytes is beta (server rollout). | | resize(input, { width?, height?, scale?, keep_aspect_ratio?, optimize? }) | POST /api/v1/images/resize | At least one of width/height/scale. | | crop(input, { x, y, width, height }) | POST /api/v1/images/crop | | | convert(input, { format, quality_mode? }) | POST /api/v1/images/convert | Beta (server rollout). format: avif, webp, jpeg, png. | | usage() | GET /api/v1/usage | { plan, period_start, period_end, included, reserved, used, remaining }. | | download(resultOrUrl) | result download_url | Returns a Blob. URLs are unauthenticated and expire after 2 hours. |

Every call resolves to { data, requestId, rateLimit }:

const { data, requestId, rateLimit } = await client.compress(input);
// rateLimit: { limit, remaining, reset } from the X-RateLimit-* headers (or null)
// requestId: quote this when contacting support

lossless is rejected for JPEG inputs (lossless_not_supported) because JPEG has no pixel-preserving lossless mode — the API refuses to pretend otherwise.

When compress runs with target_size_bytes, the result includes target_size_achieved: true when the output met the byte budget, false when it could not. The field is absent otherwise.

Batch lifecycle

Batches (Developer/Pro plans) process up to 200 files of up to 40 MB each (JPEG, PNG, WebP, AVIF) durably on the server. The operation is compress, resize, crop, or convert, and options mirror the synchronous endpoints — convert requires format (avif, webp, jpeg, png; files already in the target format fail per job with same_format_conversion), and compress accepts quality_mode plus target_size_bytes (skipped per job when it is not smaller than that job's input):

createBatch(manifest)          201  status: awaiting_upload
        |                           returns uploads[]: { file_id, client_id, upload_url, headers }
        v
uploadBatchFiles(session, files)    PUT raw bytes to each presigned upload_url
        |                           (exact headers passed through, NO Authorization header)
        v
commitBatch(id)                202  status: queued
        |
waitForBatch(id)               polls getBatch(id): queued -> processing -> terminal
        |                           terminal: succeeded | partially_succeeded | failed
        |                                     | canceled | expired
        v
downloadBatchArchive(id)       200  ZIP of the successful results
const session = await client.createBatch({
  operation: "compress",
  options: { quality_mode: "balanced" },
  files: [
    { client_id: "hero", filename: "hero.png", size_bytes: 812_331, content_type: "image/png" },
    { client_id: "logo", filename: "logo.jpg", size_bytes: 41_022, content_type: "image/jpeg" },
  ],
});

await client.uploadBatchFiles(session.data, {
  hero: "./hero.png",
  logo: "./logo.jpg",
}); // concurrency 4 by default

await client.commitBatch(session.data.id);
const finished = await client.waitForBatch(session.data.id); // 1s polls growing x1.5, cap 10s, 10 min budget

for (const file of finished.data.files) {
  console.log(file.client_id, file.status, file.original_bytes, "->", file.result_bytes);
}
const zip = await client.downloadBatchArchive(session.data.id);

Notes:

  • size_bytes and content_type in the manifest must exactly match the bytes you upload — mismatches fail the commit with uploads_incomplete (the offending client_ids are listed in error.details.client_ids).
  • Each client_id must be unique (duplicate_client_id).
  • Batch download URLs and archives follow the same 2-hour expiry as synchronous results.
  • cancelBatch is idempotent; canceling a finished batch is a no-op.

Retries and idempotency

  • Every mutating call automatically sends an Idempotency-Key header (a fresh UUID per logical call, reused across retries of that call). Override it with { idempotencyKey: "your-key" } (8–160 chars) to make your own retries safe across process restarts.
  • The client retries only 429, 503, and network failures — never any other 4xx/5xx.
  • Retry-After (seconds or HTTP-date) is honored when present; otherwise exponential backoff with full jitter: random(0, min(8s, 500ms * 2^attempt)).
  • Reusing an idempotency key with a different request body yields idempotency_conflict (409).

Errors

All API failures throw TinifyApiError with status, code, message, requestId, details, and (on 429/503) retryAfter seconds. Transport failures throw TinifyNetworkError; per-attempt timeouts and exhausted waitForBatch budgets throw TinifyTimeoutError. All extend TinifyError.

import { TinifyApiError } from "@tinify-dev/client";

try {
  await client.compress(input);
} catch (error) {
  if (error instanceof TinifyApiError) {
    console.error(error.code, error.status, error.requestId);
  }
}

Error codes

| Code | HTTP | Meaning | | --- | --- | --- | | missing_authorization | 401 | No Authorization: Bearer <token> header. | | invalid_api_key | 401 | The API key is invalid or revoked. | | missing_identity | 401 | The request is not authenticated. | | insufficient_scope | 403 | The key lacks the required scope. | | account_unavailable | 403 | The account is unavailable (e.g. on hold). | | batch_plan_required | 403 | Batches require the Developer or Pro plan. | | invalid_request | 400 | Malformed body or parameters. | | invalid_idempotency_key | 400 | Missing or malformed Idempotency-Key (8–160 chars). | | batch_not_found | 404 | Unknown batch id (or not yours). | | idempotency_conflict | 409 | Key reused with a different request body. | | batch_not_complete | 409 | Archive requested before the batch finished. | | batch_has_no_results | 409 | The batch finished without any successful file. | | file_too_large | 413 | Image exceeds the 40 MB limit. | | unsupported_media_type | 415 | API v1 supports AVIF, WebP, JPEG, and PNG. | | validation_failed | 422 | One or more request fields are invalid. | | invalid_quality_mode | 422 | Quality mode must be balanced, best_quality, or lossless. | | lossless_not_supported | 422 | JPEG has no pixel-preserving lossless mode. | | invalid_resize | 422 | Resize needs a positive width, height, or scale. | | invalid_crop | 422 | Crop needs non-negative x/y and positive width/height. | | invalid_operation | 422 | Batch operation must be compress, resize, crop, or convert. | | batch_too_large | 422 | More files than your plan's per-batch limit. | | duplicate_client_id | 422 | Every batch file needs a unique client_id. | | uploads_incomplete | 422 | Uploaded objects missing or mismatching the manifest. | | invalid_target_format | 422 | Convert: unknown target format. (beta) | | same_format_conversion | 422 | Convert: target equals the source format. (beta) | | invalid_target_size | 422 | Compress: unusable target_size_bytes. (beta) | | target_size_conflict | 422 | Compress: target_size_bytes conflicts with the quality mode. (beta) | | quota_exhausted | 429 | Monthly quota used up; check usage() and Retry-After. | | too_many_upload_sessions | 429 | Too many concurrent upload sessions. | | upload_session_storage_limit | 429 | Upload session storage limit reached. | | internal_error | 500 | Unexpected server failure — quote the requestId. |

The code type is an open union (KnownTinifyErrorCode | string), so new server codes never break your compile.

Limits

  • 40 MB (41,943,040 bytes) and 50 MP per image.
  • 200 files per batch (plan-dependent, lower on some plans).
  • Result download URLs expire after 2 hours — download promptly or re-run.
  • Rate-limit state is exposed on every response via rateLimit (X-RateLimit-Limit / -Remaining / -Reset).

CLI

The package ships a tinify-dev binary:

export TINIFY_API_KEY=tnf_live_...

tinify-dev compress *.png                        # writes photo.min.png next to each input
tinify-dev compress --quality lossless --out dist/ img/*.png
tinify-dev resize --width 800 photo.jpg
tinify-dev crop --x 0 --y 0 --width 600 --height 400 photo.png
tinify-dev convert --format webp photo.png       # beta
tinify-dev usage

Prints a per-file saved-bytes table, warns and skips files over 40 MB, and exits 1 if any file fails. Outputs are written as <name>.min.<ext> beside the input (or into --out <dir>); the CLI never overwrites your originals.

Browser usage (read this first)

The API rejects untrusted browser origins; use it server-side. CORS is only allowed for trusted first-party origins, so calls from arbitrary web apps will fail with a 403 — proxy through your backend instead. (Keys in browser bundles are public anyway.)

TypeScript notes

  • Ships .d.ts (ESM) and .d.cts (CJS) — correct types under both "module": "NodeNext" and bundlers.
  • TinifyResponse<T>, ImageResultData, Batch, Usage, and every option type are exported.
  • Response field names mirror the wire format (snake_case); client-side concepts (requestId, rateLimit) are camelCase.
  • convert() and target_size_bytes are marked @beta in TSDoc until the server rollout completes.

License

MIT © Stian Larsen