DEV Community

Cover image for Building an image processing pipeline with the Watermark Removal API
Lucas Santos Rodrigues
Lucas Santos Rodrigues

Posted on

Building an image processing pipeline with the Watermark Removal API

If you work with product photos, real estate listings, or any user-generated content at scale, chances are you've dealt with watermarked images. Removing them manually doesn't scale. This post walks through building a simple but production-ready image processing pipeline using the Goodbye Watermark API — available on RapidAPI.

The API accepts an image (URL or base64), runs it through an AI inpainting model, and returns a clean PNG. No UI, no manual steps — just HTTP.


What we're building

A pipeline that:

  1. Takes a list of image URLs
  2. Sends each one to the Watermark Removal API
  3. Saves the cleaned images to disk

Simple, composable, and easy to drop into any existing workflow.


cURL

Before writing any code, let's confirm the API works:

curl -X POST \
  https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark \
  -H "x-rapidapi-key: YOUR_RAPIDAPI_KEY" \
  -H "x-rapidapi-host: goodbye-watermark-api.p.rapidapi.com" \
  -H "Content-Type: application/json" \
  -d '{ "image": "https://example.com/photo-with-watermark.jpg" }'
Enter fullscreen mode Exit fullscreen mode

You'll get back:

{
  "success": true,
  "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
  "format": "png"
}
Enter fullscreen mode Exit fullscreen mode

The image field is a data URI. From here, you can decode the base64 and write it to disk.


Node.js pipeline

import fs from "fs";
import fetch from "node-fetch";

const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;

const images = [
  "https://example.com/product-1.jpg",
  "https://example.com/product-2.jpg",
  "https://example.com/product-3.jpg",
];

async function removeWatermark(imageUrl) {
  const res = await fetch(
    "https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark",
    {
      method: "POST",
      headers: {
        "x-rapidapi-key": RAPIDAPI_KEY,
        "x-rapidapi-host": "goodbye-watermark-api.p.rapidapi.com",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ image: imageUrl }),
    }
  );

  const data = await res.json();
  if (!data.success) throw new Error(data.error);
  return data.image; // data URI
}

function saveDataUri(dataUri, filename) {
  const base64 = dataUri.replace(/^data:image\/png;base64,/, "");
  fs.writeFileSync(filename, Buffer.from(base64, "base64"));
}

async function runPipeline() {
  for (let i = 0; i < images.length; i++) {
    console.log(`Processing image ${i + 1}/${images.length}...`);
    try {
      const result = await removeWatermark(images[i]);
      saveDataUri(result, `output-${i + 1}.png`);
      console.log(`✓ Saved output-${i + 1}.png`);
    } catch (err) {
      console.error(`✗ Failed: ${err.message}`);
    }
  }
}

runPipeline();
Enter fullscreen mode Exit fullscreen mode

Python pipeline

import os
import base64
import requests

RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]

images = [
    "https://example.com/product-1.jpg",
    "https://example.com/product-2.jpg",
    "https://example.com/product-3.jpg",
]

def remove_watermark(image_url):
    response = requests.post(
        "https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark",
        headers={
            "x-rapidapi-key": RAPIDAPI_KEY,
            "x-rapidapi-host": "goodbye-watermark-api.p.rapidapi.com",
            "Content-Type": "application/json",
        },
        json={"image": image_url},
        timeout=60,
    )
    data = response.json()
    if not data.get("success"):
        raise Exception(data.get("error", "Unknown error"))
    return data["image"]  # data URI

def save_data_uri(data_uri, filename):
    base64_data = data_uri.split(",", 1)[1]
    with open(filename, "wb") as f:
        f.write(base64.b64decode(base64_data))

for i, url in enumerate(images):
    print(f"Processing image {i + 1}/{len(images)}...")
    try:
        result = remove_watermark(url)
        filename = f"output-{i + 1}.png"
        save_data_uri(result, filename)
        print(f"✓ Saved {filename}")
    except Exception as e:
        print(f"✗ Failed: {e}")
Enter fullscreen mode Exit fullscreen mode

PHP pipeline

<?php

$apiKey = getenv("RAPIDAPI_KEY");

$images = [
    "https://example.com/product-1.jpg",
    "https://example.com/product-2.jpg",
    "https://example.com/product-3.jpg",
];

function removeWatermark(string $imageUrl, string $apiKey): string {
    $ch = curl_init("https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_HTTPHEADER     => [
            "x-rapidapi-key: $apiKey",
            "x-rapidapi-host: goodbye-watermark-api.p.rapidapi.com",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode(["image" => $imageUrl]),
    ]);

    $response = json_decode(curl_exec($ch), true);
    curl_close($ch);

    if (!$response["success"]) {
        throw new Exception($response["error"] ?? "Unknown error");
    }

    return $response["image"]; // data URI
}

function saveDataUri(string $dataUri, string $filename): void {
    $base64 = explode(",", $dataUri, 2)[1];
    file_put_contents($filename, base64_decode($base64));
}

foreach ($images as $i => $url) {
    $num = $i + 1;
    echo "Processing image $num/" . count($images) . "...\n";
    try {
        $result = removeWatermark($url, $apiKey);
        $filename = "output-$num.png";
        saveDataUri($result, $filename);
        echo "✓ Saved $filename\n";
    } catch (Exception $e) {
        echo "✗ Failed: " . $e->getMessage() . "\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

Error handling tips

The API returns structured errors on non-2xx responses:

{ "success": false, "error": "human-readable description" }
Enter fullscreen mode Exit fullscreen mode

A few things worth handling in production:

  • 429 — you've hit your plan's rate limit. Add a delay between requests or upgrade your plan.
  • 500 — upstream inference failure. Safe to retry with exponential backoff.
  • 503 — the API is temporarily down. Hit /api/v1/ping to check status before retrying.

What's next

This pipeline covers the basics. From here you can:

  • Process local files — read from disk and send as base64 instead of URL
  • Fan out in parallel — use Promise.all in Node.js or ThreadPoolExecutor in Python for faster batch processing
  • Upload to CDN — pipe the output directly to S3, Cloudflare R2, or any storage provider instead of saving to disk

Try it

The Goodbye Watermark API is live on RapidAPI with a pay-per-use model — no subscription lock-in.

👉 goodbye-watermark-api on RapidAPI

See more here: Goodbye Watermark

Top comments (0)