How to Automatically Strip Metadata from AI-Generated Images with the ExifTools API
Tutorials

How to Automatically Strip Metadata from AI-Generated Images with the ExifTools API

Bill
February 19, 2026
14 min read

Every major AI image generator now embeds metadata into its output. Some of it is obvious — prompts, seeds, model names. Some of it is less visible — cryptographically signed provenance certificates that tag every image as AI-generated. Whether you're shipping assets to clients, publishing content, or building a production pipeline, there are good reasons to strip this data before the image leaves your system.

This guide shows how to integrate the ExifTools remove API into the tools you're already using — ComfyUI, Automatic1111, the OpenAI Images API, no-code automation platforms, and plain scripts. No new infrastructure. Just a single API call inserted where your images already pass through.

What's Actually Hiding in Your AI-Generated Images?

The metadata landscape has changed significantly in the last year. Here's what the major generators embed as of early 2026:

C2PA Content Credentials

C2PA is a cryptographic provenance standard that's now the default across commercial generators. It signs a tamper-evident manifest into the file identifying the AI tool, the origin, and the generation date. The following services embed C2PA on every image by default:

  • OpenAI GPT Image 1 (replaced DALL-E) — identifies the image as originating from OpenAI/ChatGPT
  • Google Imagen 3 / Gemini — embeds C2PA plus SynthID (a separate pixel-level watermark)
  • Adobe Firefly — the most aggressive adopter, with full Content Credentials on every output
  • FLUX.2 API (Black Forest Labs) — cryptographically signed manifests on all API-generated images
  • Midjourney V7 — C2PA plus IPTC Digital Source Type tags

C2PA data adds real file size overhead — roughly 3% on PNGs from OpenAI, up to 30% on WebP files from ChatGPT.

Prompts and Generation Parameters

Open-source and some commercial tools embed your exact prompts and settings:

  • Midjourney V7 stores the full prompt (including style references) in the PNG Description text chunk. Anyone with ExifTool or a right-click can read your prompt.
  • Stable Diffusion (via Automatic1111 or Forge) writes prompt, negative prompt, sampler, CFG scale, seed, model hash, and model name to the parameters PNG text chunk.
  • ComfyUI embeds the entire workflow graph as JSON in the prompt PNG chunk — every node, every connection, every setting. Drag the PNG back into ComfyUI and it reconstructs the full workflow.

IPTC Source Type Tags

Midjourney and Shutterstock tag images with the IPTC Digital Source Type field set to trainedAlgorithmicMedia — a machine-readable flag indicating AI-generated content.

What Metadata Stripping Does Not Cover

Google's SynthID is a pixel-level watermark encoded into the image data itself, not into metadata fields. It survives format conversion, compression, editing, and metadata removal. Stripping metadata will not remove SynthID or similar pixel-level watermarks — that's a fundamentally different problem.

See It for Yourself

Upload any AI-generated image to ExifTools.com or send it to the extract endpoint and inspect what comes back. You'll likely find more than you expected.

ExifTools Remove API — Quick Reference

A single endpoint strips all metadata from any supported file:

POST https://exiftools.com/api/v1/remove

Authentication: Pass your API key in the X-API-Key header:

X-API-Key: YOUR_API_KEY_HERE

Input modes (provide exactly one):

Mode Parameter Use when
File upload file (multipart) You have the image on disk
Remote URL url (JSON body) You have a URL to the image
Base64 data_url (JSON body) You have the image in memory as base64

Response:

{
  "success": true,
  "status": "completed",
  "uuid": "123e4567-e89b-12d3-a456-426614174000",
  "downloadUrl": "https://exiftools.com/download-cleaned/123e4567-..."
}

The request is synchronous — no polling, no webhooks, no callbacks. You send the image, you get a download link back immediately. The link is valid for 3 hours.

Full documentation with examples in cURL, Python, JavaScript, PHP, and Ruby is on the API docs page.

Integration 1: OpenAI Images API — A Wrapper Function

This is the simplest integration. OpenAI's GPT Image 1 model returns base64 by default, which maps directly to the ExifTools data_url input mode. The entire integration is a few lines of code between "receive response" and "save file."

Python

import requests
import base64
from openai import OpenAI

EXIFTOOLS_API_KEY = "YOUR_EXIFTOOLS_API_KEY"

client = OpenAI()

# Generate the image
result = client.images.generate(
    model="gpt-image-1",
    prompt="A mountain landscape at golden hour",
    size="1024x1024",
    output_format="png",
)

# Strip metadata via ExifTools
b64 = result.data[0].b64_json
data_url = f"data:image/png;base64,{b64}"

clean = requests.post(
    "https://exiftools.com/api/v1/remove",
    headers={"X-API-Key": EXIFTOOLS_API_KEY},
    json={"data_url": data_url},
)

# Download the cleaned image
download = requests.get(clean.json()["downloadUrl"])
with open("clean_output.png", "wb") as f:
    f.write(download.content)

JavaScript (Node.js)

import OpenAI from "openai";
import fs from "fs";

const EXIFTOOLS_API_KEY = "YOUR_EXIFTOOLS_API_KEY";

const openai = new OpenAI();

// Generate the image
const result = await openai.images.generate({
  model: "gpt-image-1",
  prompt: "A mountain landscape at golden hour",
  size: "1024x1024",
  output_format: "png",
});

// Strip metadata via ExifTools
const b64 = result.data[0].b64_json;
const clean = await fetch("https://exiftools.com/api/v1/remove", {
  method: "POST",
  headers: {
    "X-API-Key": EXIFTOOLS_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ data_url: `data:image/png;base64,${b64}` }),
});

// Download the cleaned image
const { downloadUrl } = await clean.json();
const file = await fetch(downloadUrl);
fs.writeFileSync("clean_output.png", Buffer.from(await file.arrayBuffer()));

This strips the C2PA manifest that OpenAI embeds by default. The same pattern works for any API that returns base64 or a URL — the Black Forest Labs FLUX API, Replicate, fal.ai, or Ideogram.

Integration 2: ComfyUI — A Custom Node

ComfyUI's node-based architecture makes this the most natural integration for local generation workflows. You add a node between your generator and Save Image node. It works with any upstream model — FLUX.2, SD 3.5, SDXL, or anything else.

The Node

Save this as exiftools_strip.py inside a folder in your custom_nodes/ directory (e.g., custom_nodes/comfyui-exiftools/exiftools_strip.py):

import io
import numpy as np
import requests
import torch
from PIL import Image

class ExifToolsStripMetadata:
    """Strips all metadata from images using the ExifTools API."""

    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "strip"
    CATEGORY = "image/postprocessing"

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "api_key": ("STRING", {"default": "", "multiline": False}),
            }
        }

    def strip(self, image, api_key):
        # ComfyUI images are [B, H, W, C] float tensors in 0-1 range
        results = []

        for i in range(image.shape[0]):
            # Convert tensor to PNG bytes
            img_array = (image[i].cpu().numpy() * 255).astype(np.uint8)
            pil_image = Image.fromarray(img_array)
            buffer = io.BytesIO()
            pil_image.save(buffer, format="PNG")
            buffer.seek(0)

            # Send to ExifTools API
            response = requests.post(
                "https://exiftools.com/api/v1/remove",
                headers={"X-API-Key": api_key},
                files={"file": ("image.png", buffer, "image/png")},
            )
            data = response.json()

            if not data.get("success"):
                raise Exception(f"ExifTools API error: {data.get('error')}")

            # Download cleaned image
            cleaned = requests.get(data["downloadUrl"])
            cleaned_image = Image.open(io.BytesIO(cleaned.content)).convert("RGB")

            # Convert back to tensor
            tensor = torch.from_numpy(
                np.array(cleaned_image).astype(np.float32) / 255.0
            )
            results.append(tensor)

        return (torch.stack(results),)


NODE_CLASS_MAPPINGS = {
    "ExifToolsStripMetadata": ExifToolsStripMetadata,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "ExifToolsStripMetadata": "Strip Metadata (ExifTools)",
}

Add an __init__.py in the same folder:

from .exiftools_strip import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]

Restart ComfyUI and the node appears under image/postprocessing as "Strip Metadata (ExifTools)." Wire it between your generation output and Save Image — done. Every image that flows through the node comes out clean.

Integration 3: Automatic1111 / Forge — A Post-Processing Script

Automatic1111 and Forge both expose a postprocess_image hook that fires after every generated image. The hook gives you the PIL image in script_pp.image, and you can replace it in-place. The UI picks up the replacement automatically — no file juggling.

Save this as a .py file in your scripts/ directory:

import io
import requests
from PIL import Image
from modules import scripts

class ExifToolsStripMetadata(scripts.Script):
    def title(self):
        return "ExifTools: Strip Metadata"

    def show(self, is_img2img):
        return scripts.AlwaysVisible

    def ui(self, is_img2img):
        import gradio as gr

        with gr.Accordion("ExifTools: Strip Metadata", open=False):
            enabled = gr.Checkbox(label="Enable", value=False)
            api_key = gr.Textbox(
                label="API Key",
                placeholder="Your ExifTools API key",
                type="password",
            )

        return [enabled, api_key]

    def postprocess_image(self, p, script_pp, enabled, api_key):
        if not enabled or not api_key:
            return

        # Encode current image to PNG bytes
        buffer = io.BytesIO()
        script_pp.image.save(buffer, format="PNG")
        buffer.seek(0)

        # Send to ExifTools API
        response = requests.post(
            "https://exiftools.com/api/v1/remove",
            headers={"X-API-Key": api_key},
            files={"file": ("image.png", buffer, "image/png")},
        )
        data = response.json()

        if not data.get("success"):
            print(f"[ExifTools] Error: {data.get('error')}")
            return

        # Download and replace the image in-place
        cleaned = requests.get(data["downloadUrl"])
        script_pp.image = Image.open(io.BytesIO(cleaned.content)).convert("RGB")

After restarting the UI, expand the "ExifTools: Strip Metadata" accordion in the generation settings, check "Enable," and paste your API key. Every generated image will have its metadata stripped before it's saved — including the parameters text chunk that contains your prompt, seed, and model information.

This works identically on Forge since it inherits the A1111 Script API.

Integration 4: No-Code with n8n or Make.com

If your image generation pipeline lives in a visual automation tool, adding metadata removal is a matter of inserting one or two HTTP nodes.

n8n

n8n has a native OpenAI node with image generation support. The workflow looks like this:

  1. OpenAI Node — Generate Image action, returns image data
  2. HTTP Request Node — POST to https://exiftools.com/api/v1/remove with the image (as a file or data URL), X-API-Key in the header
  3. HTTP Request Node — GET the downloadUrl from the previous response to fetch the cleaned file
  4. Write Binary File / S3 Upload / Google Drive — Store the result wherever you need it

n8n handles binary data routing between nodes natively, so you can pass the generated image directly into the HTTP Request node as a file upload without any code.

The same pattern works for any trigger — a webhook, a scheduled run, a form submission, or a Slack command.

Make.com

In Make.com (formerly Integromat), the equivalent setup uses:

  1. A trigger module (schedule, webhook, etc.)
  2. An HTTP module to call your image generation API
  3. An HTTP module to POST the image to the ExifTools remove endpoint
  4. An HTTP module to download from the returned downloadUrl
  5. A storage module (Google Drive, Dropbox, S3, etc.)

Make.com's HTTP module supports multipart/form-data for file uploads and JSON bodies for URL or data URL input — both work with the ExifTools API.

Integration 5: Replicate and fal.ai Webhooks

If you're running FLUX or other models through Replicate or fal.ai, both platforms support webhooks that fire when a prediction completes. This gives you a natural interception point with no polling loops.

Replicate

When creating a prediction, pass a webhook URL:

import replicate

prediction = replicate.predictions.create(
    model="black-forest-labs/flux-1.1-pro",
    input={"prompt": "A mountain landscape at golden hour"},
    webhook="https://your-server.com/hooks/replicate",
    webhook_events_filter=["completed"],
)

Your webhook handler receives the prediction result, which includes the output image URL. Pass it straight to the ExifTools API:

# In your webhook handler (Flask, FastAPI, Express, etc.)
def handle_replicate_webhook(payload):
    image_url = payload["output"]  # URL to the generated image

    # Strip metadata
    clean = requests.post(
        "https://exiftools.com/api/v1/remove",
        headers={"X-API-Key": EXIFTOOLS_API_KEY},
        json={"url": image_url},
    )

    # Download and store the cleaned image
    download_url = clean.json()["downloadUrl"]
    cleaned_image = requests.get(download_url).content
    save_to_storage(cleaned_image)

fal.ai

fal.ai's queue API works the same way — pass a webhookUrl when submitting:

const result = await fal.queue.submit("fal-ai/flux/dev", {
  input: { prompt: "A mountain landscape at golden hour" },
  webhookUrl: "https://your-server.com/hooks/fal",
});

Your webhook receives the result including image URLs with url, width, height, and content_type fields. Same pattern — pass the URL to ExifTools, download the cleaned version, store it.

Integration 6: Batch Processing with a Shell Script

For one-off cleanup jobs — say you have a folder of generated images you want to strip before delivery — a simple shell script does the job:

#!/bin/bash
API_KEY="YOUR_EXIFTOOLS_API_KEY"
INPUT_DIR="./generated"
OUTPUT_DIR="./cleaned"

mkdir -p "$OUTPUT_DIR"

for file in "$INPUT_DIR"/*.{png,jpg,jpeg,webp}; do
    [ -f "$file" ] || continue

    echo "Processing: $(basename "$file")"

    # Send to ExifTools API
    response=$(curl -s -X POST "https://exiftools.com/api/v1/remove" \
        -H "X-API-Key: $API_KEY" \
        -F "file=@$file")

    download_url=$(echo "$response" | grep -o '"downloadUrl":"[^"]*"' | cut -d'"' -f4)

    if [ -n "$download_url" ]; then
        curl -s -o "$OUTPUT_DIR/$(basename "$file")" "$download_url"
        echo "  Cleaned: $OUTPUT_DIR/$(basename "$file")"
    else
        echo "  Error: $response"
    fi
done

echo "Done. Cleaned files are in $OUTPUT_DIR/"

Best Practices

Choose the right input mode. Use data_url when the image is already in memory as base64 (API-to-API flows like OpenAI). Use url when you have a remote link (webhooks from Replicate/fal.ai, CDN URLs). Use file upload for disk-based workflows (batch scripts, local generation UIs).

Keep originals if you need auditability. If your workflow requires proving provenance later, store the original alongside the cleaned version. Once metadata is stripped, it can't be recovered.

Handle errors gracefully. The API is synchronous and fast, but network issues happen. In production pipelines, wrap the call in a retry with a short backoff. Don't let a transient failure block your entire workflow.

Midjourney is the odd one out. Midjourney's Discord-based ecosystem doesn't expose hooks or an extension system. The most practical approach is to download images from the Midjourney website (which embeds the richest metadata, including prompts in the Description field) and batch-process them with a script like the one above.

Pixel-level watermarks are a separate problem. Stripping metadata removes C2PA signatures, IPTC tags, EXIF data, PNG text chunks, and XMP fields. It does not affect pixel-level watermarks like Google's SynthID, which are encoded into the image data itself and survive any amount of metadata removal.

Wrapping Up

Every major image generator in 2026 embeds metadata — whether it's OpenAI's C2PA certificates, Midjourney's prompt text chunks, or ComfyUI's full workflow JSON. The ExifTools remove API strips all of it with a single synchronous HTTP call.

The integration lives wherever your images already pass through: a wrapper function around your OpenAI API call, a node in your ComfyUI graph, a post-processing script in Automatic1111, an HTTP step in n8n, or a webhook handler for Replicate. No new infrastructure, no new dependencies — just one API call inserted at the right point in your existing pipeline.

Get your API key from your dashboard and check out the full API documentation to get started.

Resources

AI Image Generators & Metadata

Integration Platforms & Documentation