# EXIF & GPS Metadata Extractor — Bulk Image Forensics (`haketa/exif-gps-metadata-extractor`) Actor

Extract EXIF metadata, GPS coordinates, camera and lens details, timestamps, dimensions, copyright, IPTC/XMP tags and SHA-256/MD5 hashes from uploaded photos or image URLs. Fast bulk image metadata API for forensics, verification and media workflows.

- **URL**: https://apify.com/haketa/exif-gps-metadata-extractor.md
- **Developed by:** [Haketa](https://apify.com/haketa) (community)
- **Categories:** Developer tools, Automation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 📸 EXIF & GPS Metadata Extractor

#### Bulk image metadata, camera details, GPS coordinates and forensic hashes — in seconds

[![Apify Actor](https://img.shields.io/badge/Apify-Actor-00B8A9?style=for-the-badge\&logo=apify)](https://apify.com)
![EXIF](https://img.shields.io/badge/EXIF-50%2B_Tags-5B5BD6?style=for-the-badge)
![GPS](https://img.shields.io/badge/GPS-Coordinates-2E8B57?style=for-the-badge)
![Bulk](https://img.shields.io/badge/Bulk-Ready-FF8C42?style=for-the-badge)

Turn photos into clean, structured data. Add image URLs or upload a photo and get camera details, capture times, GPS coordinates, dimensions, copyright fields, raw metadata and file hashes ready for JSON, CSV, Excel or an API workflow.

No browser automation. No API key. No account on another service.

***

### ✨ What you get

| Category | Examples |
|---|---|
| 📷 Camera & lens | Make, model, lens, software, serial number |
| ⚙️ Camera settings | ISO, aperture, shutter speed, focal length, flash, white balance |
| 🕒 Capture time | Original, digitized and modified timestamps, timezone offset |
| 🗺️ GPS | Latitude, longitude, altitude, direction and Google Maps link |
| 🏙️ Address | Optional city, state, postcode and country from embedded GPS |
| 🖼️ Image properties | Format, width, height, orientation, color space and resolution |
| ✍️ Authorship | Artist, copyright, credit, title, description and keywords |
| 🔐 File integrity | SHA-256 and MD5 hashes, MIME type and byte size |
| 🧰 Advanced metadata | Complete normalized EXIF, IPTC and XMP tag object |

> One valid image creates one dataset item. Invalid URLs and unsupported files are written to the separate `ERRORS` report instead of polluting your dataset.

***

### 🚀 Start in three steps

1. Paste direct image URLs into **Image URLs**, or upload one local photo.
2. Keep the defaults for a fast run, or choose raw tags, hashes and address lookup.
3. Click **Start** and export the dataset as JSON, CSV, Excel or XML.

#### Quick example

```json
{
  "imageUrls": [
    "https://example.com/photo-1.jpg",
    "https://example.com/photo-2.heic"
  ],
  "maxItems": 1000,
  "includeRawTags": true,
  "calculateHashes": true,
  "reverseGeocode": false
}
```

For the lowest cost and highest speed, leave **Convert GPS coordinates to an address** disabled. Coordinates and Google Maps links are still returned whenever GPS exists.

***

### 🎯 Popular use cases

#### 📚 Organize a photo archive

Build a searchable catalog by camera, lens, capture date, location, dimensions or copyright owner. Export the results to a spreadsheet, database or digital asset manager.

#### 🕵️ Digital forensics and evidence intake

Capture the metadata present in submitted files and calculate SHA-256/MD5 hashes for integrity checks. Keep the original URL and final redirected URL in every record.

#### 🗺️ Map geotagged photography

Collect embedded latitude and longitude, open each point in Google Maps, or enable address lookup for a readable location.

#### 📰 Verify editorial assets

Review capture dates, device information, editing software, captions, creators and copyright tags before media publication.

#### 🛍️ Audit product and marketplace images

Find inconsistent dimensions, formats, duplicated files and residual location or device metadata across large image catalogs.

#### 🔒 Privacy review

Detect whether photos expose GPS coordinates, timestamps, device serial numbers or creator data before sharing them publicly.

#### 🧹 Find duplicate assets

Group exact duplicate files using SHA-256 or MD5 even when filenames and URLs differ.

#### 🤖 Automate AI and data pipelines

Send normalized photo metadata into an Apify integration, webhook, Make, Zapier, an AI agent or your own application.

***

### 📥 Input

| Field | Type | Default | Purpose |
|---|---:|---:|---|
| `imageUrls` | array | sample images | Direct public image URLs, one per line |
| `imageFile` | file | empty | Upload one local image in Apify Console |
| `maxItems` | integer | `1000` | Stop after this many images; `0` means the full list |
| `maxConcurrency` | integer | `10` | Parallel downloads; range `1–30` |
| `maxFileSizeMb` | integer | `25` | Skip files larger than this limit |
| `includeRawTags` | boolean | `true` | Include all parsed EXIF/IPTC/XMP tags |
| `calculateHashes` | boolean | `true` | Calculate SHA-256 and MD5 |
| `reverseGeocode` | boolean | `false` | Convert embedded GPS to a readable address |
| `proxyConfiguration` | object | off | Optional proxy for restricted image hosts |

#### Supported inputs

- Direct `http://` or `https://` image URLs
- An image uploaded through the Actor input form
- JPG and JPEG
- TIFF
- PNG with embedded metadata
- WebP with embedded metadata
- HEIC, HEIF and AVIF when the metadata container is supported

The URL must return the image itself. A webpage containing an image is not a direct image URL.

#### Bulk input

Paste hundreds or thousands of URLs, one per line:

```json
{
  "imageUrls": [
    "https://cdn.example.com/catalog/a.jpg",
    "https://cdn.example.com/catalog/b.webp",
    "https://cdn.example.com/catalog/c.png"
  ],
  "maxItems": 0,
  "maxConcurrency": 10
}
```

Duplicate URLs inside the same run are automatically removed.

***

### 📤 Output

Each successfully downloaded image produces one structured record:

```json
{
  "sourceUrl": "https://example.com/photo.jpg",
  "finalUrl": "https://cdn.example.com/photo.jpg",
  "fileName": "photo.jpg",
  "file": {
    "format": "JPEG",
    "mimeType": "image/jpeg",
    "sizeBytes": 708769,
    "sizeMegabytes": 0.676,
    "sha256": "9134...e24a",
    "md5": "278a...542b"
  },
  "image": {
    "width": 4032,
    "height": 3024,
    "orientation": "Horizontal (normal)",
    "colorSpace": "sRGB"
  },
  "device": {
    "make": "Apple",
    "model": "iPhone 15 Pro Max",
    "lensModel": "iPhone 15 Pro Max back triple camera"
  },
  "capture": {
    "dateTimeOriginal": "2025-03-08T09:45:35.000Z",
    "timezoneOffset": "+07:00"
  },
  "camera": {
    "exposureTimeSeconds": 0.00008,
    "aperture": 1.78,
    "iso": 64,
    "focalLengthMm": 6.765
  },
  "gps": {
    "hasCoordinates": true,
    "latitude": 13.722686,
    "longitude": 100.581619,
    "altitudeMeters": 6.961,
    "googleMapsUrl": "https://www.google.com/maps?q=13.722686,100.581619"
  },
  "authorship": {
    "artist": "Example Photographer",
    "copyright": "Copyright 2026"
  },
  "rawTags": {},
  "processedAt": "2026-08-03T12:00:00.000Z"
}
```

Fields appear only when the source file contains them. A blank camera or GPS field usually means the image has no such embedded metadata—not that the extraction failed.

#### Additional run outputs

- **Dataset** — one clean item per successfully processed image
- **SUMMARY** — requested, successful and skipped counts plus success rate
- **ERRORS** — invalid URLs, download errors, webpages and oversized files

***

### 🧭 GPS and address behavior

GPS coordinates are read directly from the image metadata. The Actor never estimates a location from visual content.

When coordinates exist, the result includes:

- Decimal latitude and longitude
- Altitude when recorded
- Camera direction when recorded
- GPS date and datum when recorded
- A clickable Google Maps URL

Enable `reverseGeocode` only when you need a readable address. Address lookup uses OpenStreetMap data and is deliberately rate-limited; coverage and precision depend on the coordinates and map data available. © OpenStreetMap contributors.

***

### ⚡ Performance and cost tips

- Keep `reverseGeocode: false` for large batches.
- Keep `maxConcurrency` around `10` for a good speed/memory balance.
- Reduce `maxFileSizeMb` when processing thumbnails or web catalogs.
- Disable `includeRawTags` if you only need the clean, common fields.
- Disable `calculateHashes` only when hash-based deduplication is unnecessary.
- Use direct image URLs to avoid redirects and failed webpage downloads.
- Use a proxy only when the image host actually requires it.

The Actor uses lightweight file downloads and metadata parsing rather than a browser, keeping runs fast and economical.

***

### 🔌 API

#### Start a run

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_ACTOR_ID/runs?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "imageUrls": ["https://example.com/photo.jpg"],
    "includeRawTags": true,
    "calculateHashes": true
  }'
```

#### Run synchronously and receive results

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_ACTOR_ID/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"imageUrls":["https://example.com/photo.jpg"]}'
```

The same Actor can be called through JavaScript, Python, the Apify CLI, MCP, Make, Zapier or a webhook.

***

### ❓ FAQ

#### Why is GPS missing?

Many social networks, messaging apps and image editors strip location metadata during upload or export. The original camera file is more likely to contain GPS.

#### Does the Actor identify a location from image pixels?

No. It reads embedded GPS only. This makes the result predictable and avoids costly AI inference.

#### Does a processed image always contain EXIF?

No. PNG screenshots, optimized web assets and edited images often have little or no EXIF. You still receive file properties, format, size and hashes.

#### Are errors included in the dataset?

No. Failed downloads and invalid files go to the `ERRORS` output so the primary dataset stays clean.

#### Can I process private images?

Upload a file in Apify Console, or provide a temporary signed URL that the Actor can access. Never place long-lived credentials in a public URL.

#### Does it change or strip metadata?

No. The Actor is read-only: it downloads, parses and reports metadata without modifying the source image.

#### Can it find identical photos with different names?

Yes. Exact byte-for-byte duplicates have the same SHA-256 and MD5 hashes.

#### What happens with an oversized file?

The file is skipped and recorded in `ERRORS`. Increase `maxFileSizeMb` if the source is trusted and the extra memory use is acceptable.

#### Is reverse geocoding required?

No. It is optional and off by default. GPS coordinates and a map link do not require address lookup.

***

### 🔐 Privacy and responsible use

EXIF may reveal precise locations, device identifiers, timestamps and creator information. Process only images you are authorized to access, protect exported datasets appropriately, and follow applicable privacy, copyright and data-protection laws.

The Actor reads user-supplied public URLs or uploaded files. It does not discover private photos, bypass authentication or modify source content.

***

### 💬 Support

If a supported image returns unexpected fields, include a reproducible public sample URL and the run ID in an Actor issue. Do not post private photographs or credentials in a public issue.

Ready to inspect your photos? Add the URLs, click **Start**, and download structured EXIF and GPS metadata in seconds. 🚀

# Actor input Schema

## `imageUrls` (type: `array`):

Paste direct public image URLs, one per line. JPG, JPEG, TIFF, PNG, WebP, HEIC and AVIF are supported when metadata is embedded.

## `imageFile` (type: `string`):

Optional file upload. Use this for a local photo instead of a public URL.

## `maxItems` (type: `integer`):

Maximum number of images to process. Set to 0 to process the complete URL list.

## `maxConcurrency` (type: `integer`):

Number of images downloaded at once. The default is fast without creating unnecessary memory pressure.

## `maxFileSizeMb` (type: `integer`):

Skip unexpectedly large files to keep the run fast and affordable.

## `includeRawTags` (type: `boolean`):

Keep the complete normalized EXIF, IPTC and XMP tag object alongside the clean fields.

## `calculateHashes` (type: `boolean`):

Add content hashes for duplicate detection, evidence integrity and asset verification.

## `reverseGeocode` (type: `boolean`):

When GPS exists, add a human-readable address. This is slower and is disabled by default for economical bulk runs.

## `proxyConfiguration` (type: `object`):

Optional Apify Proxy configuration for image hosts that block direct downloads.

## Actor input object example

```json
{
  "imageUrls": [
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/canon-dslr.jpg",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/IMG_20180725_163423-tiny.jpg",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/png/png_with_exif_and_gps.png",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/tif-with-iptc-icc-xmp.tif"
  ],
  "maxItems": 20,
  "maxConcurrency": 8,
  "maxFileSizeMb": 25,
  "includeRawTags": true,
  "calculateHashes": true,
  "reverseGeocode": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `errors` (type: `string`):

No description

## `summary` (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 = {
    "imageUrls": [
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/canon-dslr.jpg",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/IMG_20180725_163423-tiny.jpg",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/png/png_with_exif_and_gps.png",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/tif-with-iptc-icc-xmp.tif"
    ],
    "maxItems": 20,
    "maxConcurrency": 8,
    "reverseGeocode": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("haketa/exif-gps-metadata-extractor").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 = {
    "imageUrls": [
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/canon-dslr.jpg",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/IMG_20180725_163423-tiny.jpg",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/png/png_with_exif_and_gps.png",
        "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/tif-with-iptc-icc-xmp.tif",
    ],
    "maxItems": 20,
    "maxConcurrency": 8,
    "reverseGeocode": False,
}

# Run the Actor and wait for it to finish
run = client.actor("haketa/exif-gps-metadata-extractor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "imageUrls": [
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/canon-dslr.jpg",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/IMG_20180725_163423-tiny.jpg",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/png/png_with_exif_and_gps.png",
    "https://raw.githubusercontent.com/MikeKovarik/exifr/master/test/fixtures/tif-with-iptc-icc-xmp.tif"
  ],
  "maxItems": 20,
  "maxConcurrency": 8,
  "reverseGeocode": false
}' |
apify call haketa/exif-gps-metadata-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=haketa/exif-gps-metadata-extractor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MgaGVKwjfO0ODhsxM/builds/2GyVCM6U4L8Uk2Ive/openapi.json
