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:
- Takes a list of image URLs
- Sends each one to the Watermark Removal API
- 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" }'
You'll get back:
{
"success": true,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"format": "png"
}
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();
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}")
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";
}
}
Error handling tips
The API returns structured errors on non-2xx responses:
{ "success": false, "error": "human-readable description" }
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/pingto 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.allin Node.js orThreadPoolExecutorin 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)