# Image Normalization API (`nexgenwatch/image-normalization-api`) Actor

Normalizes each supplied image — EXIF auto-rotate, resize to a max dimension, format convert, compress, strip unsafe metadata — returning the artifact plus before/after dimensions, color mode, MIME, byte sizes, and SHA-256s. Upload-driven; no discovery source.

- **URL**: https://apify.com/nexgenwatch/image-normalization-api.md
- **Developed by:** [NexGen Watch](https://apify.com/nexgenwatch) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $33.50 / 1,000 image normalization api verdicts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Image Normalization API

Normalizes each supplied image — auto-rotate by EXIF, resize to a max dimension, convert format, compress, and strip unsafe metadata — and returns the normalized artifact plus before/after dimensions, color mode, MIME, byte sizes, and SHA-256s. Upload-driven; there is no search or discovery source.

### What you submit

`images` — images (base64, data: url, kvs key, or permitted image url). Each item is an image supplied as a base64 string, a data: URL, a key naming an entry in the run key-value store, or a permitted direct http(s) image URL (URL items pass the per-origin runtime gate — robots + SSRF — before any fetch; there is no search/discovery source). Per image ONE normalized artifact (stored in the run key-value store) plus before/after dimensions, color mode, MIME, byte sizes, SHA-256s, the operations applied, and warnings. Operations: auto-rotate by EXIF, resize to a max dimension, convert format, compress, and strip unsafe metadata. Guards: format allowlist, a pixel-count cap, and a decompression-bomb guard. A completed normalized image bills; a corrupted, oversized, or disallowed-format input, or a blocked/unreachable URL, is an unbilled status.

### The runtime source gate (per submitted target)

You choose the targets, so the source contract is enforced at run time — for every URL-mode item (base64 / data: URL / key-value-store items are local and are not fetched) — per origin, before any page is read:

- **robots.txt** fetched once per origin and honored; no robots / 404 = permitted, a disallowed path or an unavailable/forbidden robots file = **BLOCKED**.
- **SSRF guard** — every host is resolved and must be public; private, loopback, link-local, reserved and cloud-metadata addresses are refused before a socket opens.
- **HTTP** — `403` / `429` / `5xx` = **BLOCKED**; DNS / timeout / connection faults = **UNREACHABLE**.

A **BLOCKED** or **UNREACHABLE** target is delivered as an unbilled status row — never a broken-site verdict, and never charged.

### Untrusted-file guards

Every image is treated as untrusted: a byte-size cap and a pixel-count cap (checked from the declared dimensions **before** the image is decoded) guard against decompression bombs, and only an allowlisted set of input formats is accepted.

### Output & billing

One row per image. A **completed normalization** carries `outcome: answer` and bills once. A corrupted, oversized, or disallowed-format input, or a blocked / unreachable URL, is delivered as an unbilled status. Push-then-charge: every row is delivered **before** its charge, so a billing hiccup can only ever undercharge.

### Pricing

| Event | Price | When |
| --- | --- | --- |
| `apify-actor-start` | $0.02 | once when the run starts (reserved; never charged in code) |
| `image_normalization_check` | $0.05 | once per **completed normalized image** (rejected / blocked / unreachable never bill) |

Volume tiers reduce the per-verdict price as usage grows: $0.05 → $0.045 → $0.04 → $0.0335.

### Why this and not the obvious alternative

a deterministic normalized image artifact (auto-rotate, resize, convert, compress, strip unsafe metadata) with before/after hashes and dimensions and hard pixel/byte bomb guards — an upload-driven normalization API, not an image search or scraper.

# Actor input Schema

## `images` (type: `array`):

Each item is an image supplied as a base64 string, a data: URL, a key naming an entry in the run key-value store, or a permitted direct http(s) image URL (URL items pass the per-origin runtime gate — robots + SSRF — before any fetch; there is no search/discovery source). Per image ONE normalized artifact (stored in the run key-value store) plus before/after dimensions, color mode, MIME, byte sizes, SHA-256s, the operations applied, and warnings. Operations: auto-rotate by EXIF, resize to a max dimension, convert format, compress, and strip unsafe metadata. Guards: format allowlist, a pixel-count cap, and a decompression-bomb guard. A completed normalized image bills; a corrupted, oversized, or disallowed-format input, or a blocked/unreachable URL, is an unbilled status.

## `targetFormat` (type: `string`):

Output image format.

## `maxDimension` (type: `integer`):

Longest side is resized down to this; smaller images are left as-is.

## `quality` (type: `integer`):

JPEG/WEBP quality.

## `stripMetadata` (type: `boolean`):

Drop EXIF/ICC and other metadata from the output.

## `maxBytes` (type: `integer`):

Reject images larger than this (decompression-bomb guard).

## `maxPixels` (type: `integer`):

Reject images whose width x height exceeds this (decompression-bomb guard).

## `userAgent` (type: `string`):

Override the transparent crawler UA.

## Actor input object example

```json
{
  "images": [
    "https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg",
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
    "http://169.254.169.254/"
  ],
  "targetFormat": "PNG",
  "maxDimension": 2048,
  "quality": 85,
  "stripMetadata": true,
  "maxBytes": 26214400,
  "maxPixels": 40000000,
  "userAgent": "Mozilla/5.0 (compatible; NexGenWatchBot/1.0; +https://apify.com/nexgenwatch)"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "images": [
        "https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg",
        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
        "http://169.254.169.254/"
    ],
    "targetFormat": "PNG",
    "maxDimension": 2048,
    "quality": 85,
    "stripMetadata": true,
    "maxBytes": 26214400,
    "maxPixels": 40000000,
    "userAgent": "Mozilla/5.0 (compatible; NexGenWatchBot/1.0; +https://apify.com/nexgenwatch)"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nexgenwatch/image-normalization-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "images": [
        "https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg",
        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
        "http://169.254.169.254/",
    ],
    "targetFormat": "PNG",
    "maxDimension": 2048,
    "quality": 85,
    "stripMetadata": True,
    "maxBytes": 26214400,
    "maxPixels": 40000000,
    "userAgent": "Mozilla/5.0 (compatible; NexGenWatchBot/1.0; +https://apify.com/nexgenwatch)",
}

# Run the Actor and wait for it to finish
run = client.actor("nexgenwatch/image-normalization-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "images": [
    "https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg",
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
    "http://169.254.169.254/"
  ],
  "targetFormat": "PNG",
  "maxDimension": 2048,
  "quality": 85,
  "stripMetadata": true,
  "maxBytes": 26214400,
  "maxPixels": 40000000,
  "userAgent": "Mozilla/5.0 (compatible; NexGenWatchBot/1.0; +https://apify.com/nexgenwatch)"
}' |
apify call nexgenwatch/image-normalization-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nexgenwatch/image-normalization-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/GSHB0C9bwE37KCcQZ/builds/zKtw7Gi3ssa3qyaQX/openapi.json
