Official client libraries for Node.js/TypeScript and Python. One install, full type safety, no boilerplate.
Install
npm install @filtrate/sdk
Usage — TypeScript
import { Filtrate } from '@filtrate/sdk';
const filtrate = new Filtrate({ apiKey: process.env.FILTRATE_API_KEY });
const result = await filtrate.process({
imageUrl: 'https://example.com/product-photo.jpg',
quality: 80,
});
console.log(result.status); // 'safe' | 'explicit' | 'fraud' | 'banned_logo'
console.log(result.optimized_url); // CDN URL of processed WebP
console.log(result.size_reduction); // e.g. "67%"
Install
pip install filtrate
Usage — Python
from filtrate import Filtrate
filtrate = Filtrate(api_key="flt_live_...") # or set FILTRATE_API_KEY env var
result = filtrate.process(
image_url="https://example.com/product-photo.jpg",
quality=80,
)
print(result.status) # 'safe' | 'explicit' | 'fraud' | 'banned_logo'
print(result.optimized_url) # CDN URL of processed WebP
print(result.size_reduction) # e.g. "67%"
Prefer raw HTTP? Scroll to Code Examples below for curl, fetch, and axios snippets.
01 — CLI
Command-line interface
One command to evaluate Filtrate — no account needed for scanning. Works in your terminal or CI pipeline.
1
Scan a local or remote image (anonymous, no API key)
Uses the playground endpoint — 5 requests per IP per 15 minutes, free. Exits 1 on compliance issues, perfect for CI.
2
Process with your API key (authenticated, full tier)
Pass --key $FILTRATE_KEY or run filtrate login once to store your key.
Install
npm install -g @filtrate/cli
# or use without installing:
npx @filtrate/cli scan ./photo.jpg
Scan (anonymous)
npx @filtrate/cli scan https://example.com/listing-photo.jpg
# Or a local file:
npx @filtrate/cli scan ./product-photos/listing-42.jpg
Process (authenticated)
npx @filtrate/cli process ./photo.jpg --key flt_live_xxxxxxxxxxxxxxxx
# Save the cleaned WebP locally:
npx @filtrate/cli process ./photo.jpg --key $FILTRATE_KEY --output cleaned.webp
# Machine-readable JSON:
npx @filtrate/cli process ./photo.jpg --key $FILTRATE_KEY --json
Login (store API key once)
npx @filtrate/cli login
# After login, subsequent commands don't need --key:
npx @filtrate/cli process ./photo.jpg
Use the GitHub Action instead: For automated PR scanning, the Filtrate GitHub Action is faster to configure than the CLI.
See GitHub Action below →
02 — GitHub Action
Scan every PR automatically
The Filtrate GitHub Action diffs each pull request, scans changed images for EXIF/GPS leaks, and posts a report table as a PR comment. Blocks merge when GPS coordinates are found.
1
Add the workflow file to your repo
Create .github/workflows/filtrate-scan.yml with the snippet below.
2
Open a PR that changes an image
The action comments a scan table automatically. CI fails on GPS leaks by default — set fail-on: none to report-only.
Every API request requires an API key passed as a Bearer token in the Authorization header. Keys are scoped to your account and never expire unless you revoke them.
No auth required: The playground endpoint at POST /v1/playground lets you test the full pipeline without an API key — no signup needed.
02 — Endpoints
POST /v1/process
The primary endpoint. Upload an image file (multipart/form-data) or provide an image URL (JSON body). The response delivers a processed WebP, size stats, and an AI content moderation verdict — all in one call.
POST/v1/process
Headers
NameTypeRequiredDescription
AuthorizationstringrequiredBearer token with your API key
Content-Typestringrequired*Must be multipart/form-data when uploading a file. Omit for URL-based requests.
Request Body (multipart/form-data)
FieldTypeRequiredDescription
imagefilerequired*Image file to process. JPEG, PNG, WebP, GIF, AVIF, TIFF, HEIC accepted. Max 20 MB.
image_urlstringrequired*Remote image URL. Required only if no image field is provided.
Every successful call returns HTTP 200 with a JSON body. The response contains processing metadata, the CDN URL of the processed WebP, and a compliance verdict.
Success Response (HTTP 200)
FieldTypeDescription
statusstringOverall compliance verdict: safe, explicit, fraud, or banned_logo
optimized_urlstringCDN URL of the processed WebP file (expires never)
size_reductionstringSize reduction as a percentage, e.g. "67%"
original_size_bytesintegerOriginal image size in bytes
output_size_bytesintegerProcessed WebP size in bytes
formatstringAlways "webp"
qualityintegerWebP quality setting used for this call
exif_strippedbooleantrue — all EXIF/IPTC/XMP metadata removed and image auto-rotated
processing_msintegerTotal round-trip time in milliseconds
from_cachebooleantrue if the moderation verdict was served from cache (same image previously processed)
pipeline_timingsobjectBreakdown of time spent in each pipeline stage in milliseconds
complianceobjectAI content moderation results — see sub-fields below
compliance sub-object
FieldTypeDescription
verdictstringSame as status at top level: safe | explicit | fraud | banned_logo
categoriesobjectBoolean flags for each detection category
errorstringPresent only when AI moderation fails. The image is still processed and returned.
When something goes wrong, the API returns an appropriate HTTP status code and a JSON body with an error field describing what happened.
StatusNameDescription
400Bad RequestMissing or invalid input — no image provided, unsupported format, or failed to fetch the remote URL
401UnauthorizedNo API key provided, or the key is invalid or revoked
413Payload Too LargeFile exceeds the 20 MB limit. Reduce the image size before uploading.
429Too Many RequestsRate limit exceeded (100 req/min per API key). Check the Retry-After header for the number of seconds to wait.
500Internal Server ErrorUnexpected processing failure. Retry the request. If the problem persists, contact support.
Error Response Body
400 Bad Request — example error body
{
"error": "Provide either image_url in JSON body or an image file as multipart field 'image'"
}
Rate Limit Headers
HeaderTypeDescription
X-RateLimit-LimitintegerMax requests allowed per minute for this API key
X-RateLimit-RemainingintegerRequests remaining in the current 60-second window
Retry-AfterintegerSeconds to wait before retrying (only present on 429 responses)
04 — Batch Async
Async batch processing
For high-volume pipelines processing millions of images, POST /v1/batch accepts up to 1,000 image URLs and returns immediately with a batch_id. A background worker processes images every 2 minutes — results are uploaded to R2 and a webhook fires on completion. Each image counts as 1 API call at $0.002.
When a batch completes, Filtrate POSTs to your callback_url with a JSON body signed using HMAC-SHA256. The signature is in the X-Filtrate-Signature header.
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_signature(raw_body: bytes, sig_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
expected = hmac.new(
secret.encode(), f"{parts['t']}.{raw_body.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
@app.route("/webhooks/filtrate", methods=["POST"])
def webhook():
raw = request.get_data()
secret = os.environ["FILTRATE_WEBHOOK_SECRET"]
if not verify_signature(raw, request.headers.get("X-Filtrate-Signature", ""), secret):
abort(401)
payload = request.json
print(f"Batch {payload['batch_id']} completed")
return "", 200
SDK usage
Node.js — batch.create() + batch.get()
import { Filtrate } from '@filtrate/sdk';
const filtrate = new Filtrate({ apiKey: process.env.FILTRATE_API_KEY });
// Submit batch — returns immediately with batch_id
const queued = await filtrate.batchCreate({
images: [
'https://cdn.example.com/listings/img-001.jpg',
'https://cdn.example.com/listings/img-002.jpg',
],
callbackUrl: 'https://yourapp.com/webhooks/filtrate',
metadata: { upload_batch: 'spring-2026' },
});
console.log(queued.batch_id); // 'batch_a3f1c9d82e0b4f7c'
console.log(queued.estimated_completion_seconds); // e.g. 15
// Poll until complete
let status;
do {
await new Promise(r => setTimeout(r, 5000));
status = await filtrate.batchGet(queued.batch_id);
console.log(`Progress: ${status.progress.completed}/${status.progress.total}`);
} while (status.status === 'queued' || status.status === 'processing');
// Results are at status.results_url — fetch it
if (status.results_url) {
const results = await fetch(status.results_url).then(r => r.json());
console.log(results.batch_id); // batch_id
console.log(results.results); // array of per-image responses
}
Python — batch_create() + batch_get()
from filtrate import FiltrateClient
client = FiltrateClient(api_key="flt_live_...")
# Submit batch
queued = client.batch_create(
images=[
"https://cdn.example.com/listings/img-001.jpg",
"https://cdn.example.com/listings/img-002.jpg",
],
callback_url="https://yourapp.com/webhooks/filtrate",
metadata={"upload_batch": "spring-2026"},
)
print(queued.batch_id) # 'batch_a3f1c9d82e0b4f7c'
# Poll until complete
import time
status = None
while True:
status = client.batch_get(queued.batch_id)
print(f"Completed: {status.progress.completed}/{status.progress.total}")
if status.status in ("completed", "failed", "partial"):
break
time.sleep(5)
if status.results_url:
import httpx
results = httpx.get(status.results_url).json()
print(results["batch_id"])
print(results["results"]) # list of per-image responses
Rate limit: Max 5 concurrent active batches per account. The API returns 429 if you exceed this. Wait for a batch to complete (or reach completed/failed status) before submitting another.
05 — Code Examples
Integrate in minutes
Copy the example that matches your stack. Replace YOUR_API_KEY with your actual key from the dashboard.
If your use case requires more than 100 req/min, contact us. We can provision higher throughput for high-volume customers.
No minimum. Filtrate is pay-as-you-go at $0.002/image with no monthly commitment. No credits to track — just sign up, generate an API key, and start processing.
08 — Drop-in Middleware
Drop-in middleware packages
Stop writing multipart-parsing-and-scan wrappers. Install one package, add one line, done. Filtrate handles EXIF strip, WebP compression, and AI moderation — results land on req.filtrate or ctx before your handler runs.
$0.002/image. Same pricing as the raw API — no middleware surcharge.
install
npm install @filtrate/express multer
upload.js
import multer from 'multer';
import { filtrate } from '@filtrate/express';
const upload = multer({ storage: multer.memoryStorage() });
// Drop in. One line.
app.post('/upload',
upload.any(),
filtrate({ apiKey: process.env.FILTRATE_API_KEY }),
(req, res) => {
// Blocked uploads never reach here
const urls = req.filtrate.results.map(r => r.optimized_url);
res.json({ ok: true, urls });
}
);
Add EXIF/GPS scanning to any repo in 5 lines of YAML. Every PR that touches image files gets a compliance report as a comment — GPS leaks are caught before merge.
name: Image Privacy Scan
on: pull_request
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # required to post the PR comment
steps:
- uses: actions/checkout@v4
- uses: Polsia-Inc/filtrate-scan-action@v1
with:
api-key: ${{ secrets.FILTRATE_API_KEY }}
fail-on: gps # fail CI when GPS coordinates are found
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
That's it. The action diffs the PR for changed image files, calls the Filtrate scan API, posts a results table as a PR comment, and exits non-zero if GPS coordinates are found.
Inputs
Input
Required
Default
Description
api-key
No
—
Your Filtrate API key. Optional — anonymous scanning works without a key (rate-limited).
fail-on
No
gps
gps = fail on GPS, any = fail on any EXIF, none = report only.