Perceptual hash
Two photos can be pixel-for-pixel different — resized, re-saved, watermarked — and still be visually the same image. This endpoint generates a perceptual hash that captures what an image looks like, not its exact bytes, so you can compare pictures the way a human eye would.
Run — free
Runs in your browser. Free, unlimited — your data never leaves this page.
Why byte comparison isn't enough
A checksum or file hash changes completely the moment a single byte is different, which makes it useless for catching the same photo re-uploaded at a different resolution, re-encoded at a different quality level, or cropped by a few pixels. Yet that's exactly the kind of duplication that shows up constantly in the real world: a stolen product photo resized to fit a new listing, a stock image re-compressed by a dozen different platforms, a screenshot of a screenshot. A perceptual hash is built to survive exactly those transformations.
What the hash actually captures
Send an image to POST /image/phash and the task analyzes its overall structure — the broad pattern of light and dark regions — and returns a compact hash value describing that pattern rather than the raw pixels. Two images that look alike to a person produce hashes that are close to each other by a simple distance measure, even if their file sizes, dimensions or compression differ substantially; two genuinely different images produce hashes that are far apart.
A technique older than the web
Perceptual hashing descends from a much older field: content-based image retrieval, which researchers were working on well before search engines could index images by their surrounding text. The core insight hasn't changed since — reduce an image to its coarse structure, discard the fine detail that varies with compression and editing, and you get a fingerprint that's robust to the noise while still being distinctive between genuinely different photos.
Comparing hashes, not images
The output of this endpoint is a hash value, not a similarity score against another specific image — that comparison happens on your side, computing the distance between two hashes you've already generated, which is fast enough to run across large collections without calling the API again. As with every task here, the request itself is asynchronous: you get a task_id immediately and the hash arrives via signed webhook or a signed link valid for 24 hours.
Putting it to work at scale
Platforms typically hash every image on ingestion and store the value alongside the file, then compare new uploads against the existing set to catch duplicates, reposts or copyright violations before they go live. At $0.002 per request and no charge for a failed task, hashing an entire existing library to backfill a duplicate-detection system is affordable enough to do in one pass rather than sampling a subset.
What you can do with it
Duplicate upload detection
Hash every new upload and compare it against existing hashes to catch the same photo re-submitted at a different size or compression level.
Stolen or reposted content flagging
Detect when a product photo or article image has been lifted and re-uploaded elsewhere, even after resizing or light editing.
Marketplace listing deduplication
Identify near-identical listing photos across sellers to merge duplicate catalog entries or flag suspicious relisting behavior.
Media library cleanup
Backfill perceptual hashes across an existing photo archive to find and consolidate near-duplicate files accumulated over years of uploads.
FAQ
How is a perceptual hash different from a regular file hash?
A file hash changes completely with any byte-level change; a perceptual hash stays similar for images that look alike even after resizing, recompression or minor edits.
Is the perceptual hash API free to use?
The tool above runs free in your browser. The API is paid — each call draws from your prepaid ForHosting KIT balance: top up from $10.00 (it never expires), pay each request's published price, and a call with no balance returns HTTP 402. No subscription, no tokens, and a failed task is never charged.
Does the API tell me if two images are duplicates?
The API returns a hash value for each image; you compute the distance between two hashes yourself to decide how similar they are, which is fast and doesn't require another API call.
Will it catch resized or recompressed duplicates?
Yes, that's the core purpose of a perceptual hash — resizing, recompression and minor edits produce a hash that stays close to the original.
Can I hash an entire existing image library in bulk?
Yes, submit one request per image; each task is queued and billed independently, so backfilling hashes across a large archive is straightforward.
How do I receive the generated hash?
Via a signed webhook, recommended for automated pipelines, or a signed link valid for 24 hours.
Will a completely different image ever match by mistake?
Genuinely different images produce hashes far apart under standard distance measures, so false matches are uncommon when you use a sensible similarity threshold.
What image formats does phash support?
Standard raster formats such as JPEG and PNG are supported for generating the perceptual hash.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/image/phash \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"image":"https://ejemplo.com/imagen.jpg"}'const res = await fetch("https://api.kit.forhosting.com/image/phash", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"image": "https://ejemplo.com/imagen.jpg"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/image/phash",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"image": "https://ejemplo.com/imagen.jpg"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/image/phash", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"image":"https://ejemplo.com/imagen.jpg"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"image":"https://ejemplo.com/imagen.jpg"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/image/phash", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"image": "https://ejemplo.com/imagen.jpg"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "image.phash",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 15 |
max_megapixels | 12 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |