API Reference
Complete reference for all SimplyPNG API endpoints.
Base URL
https://api.simplypng.app/api/v1Headers
All requests require the following headers:
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_API_KEY | Yes |
Content-Type | application/json | Yes (for POST) |
Idempotency-Key | Unique string (max 255 chars) | No |
/jobsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
image | string | Yes | Image URL (recommended) or base64 data URL. Supported formats: JPEG, PNG, WebP, HEIC/HEIF. |
output_mode | string | No | download_url (default) or base64_json |
idempotency_key | string | No | Unique key to prevent duplicate jobs. Also accepted via Idempotency-Key header (max 255 chars). |
options.hd_mode | boolean | No | Enable HD processing (2 credits per image). Preserves images up to 4096px. Without HD mode, images are optimized to 2500px max dimension for faster processing. |
options.max_dimension | number | No | Override the default input resize target (512–4096). Default: 2500px (standard) or 4096px (HD mode). Use this when you need a specific output resolution. |
options.output_type | string | No | original (default) or centered. Centered places the subject on a canvas. |
options.background | string | No | transparent (default), white, or custom. Note: centered + transparent auto-corrects to white. |
options.background_color | string | Conditional | Required when background is custom. Hex color with # prefix (e.g., #FF5733 or #F00). |
options.output_format | string | No | png (default) or jpg. JPEG does not support transparency; if jpg + transparent, background auto-corrects to white. |
options.jpeg_quality | number | No | JPEG quality, 50–100 (default: 85). Only applies when output_format is jpg. |
options.canvas_size_preset | string | No | Only for centered output. Values: small (1000×1000), medium (1500×1500), standard (2000×2000, default), optimal (2500×2500), large (3000×3000), maximum (4096×4096), custom. |
options.canvas_width | number | No | Custom canvas width in pixels. Only when canvas_size_preset is custom. |
options.canvas_height | number | No | Custom canvas height in pixels. Only when canvas_size_preset is custom. |
options.fill_ratio_preset | string | No | Only for centered output. Values: spacious (80%), standard (85%, default), tight (90%). |
options.fill_ratio | number | No | Custom fill ratio (0.5–0.95). Overrides fill_ratio_preset. Only for centered output. |
Example Request (URL Input - Recommended)
curl -X POST https://api.simplypng.app/api/v1/jobs \
-H "Authorization: Bearer sp_test_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://your-storage.com/photo.jpg",
"output_mode": "download_url",
"idempotency_key": "unique-request-id-123"
}'URL input is recommended for large images (up to 30MB). For base64, use data:image/png;base64,... format (max ~4.5MB payload). Images above ~3000px should use URL input to avoid payload limits.
Input Image Optimization
By default, input images larger than 2500px are resized to 2500px (longest edge) before processing. This exceeds all major e-commerce marketplace requirements (Amazon 2000px, Shopify 2048px, Etsy 2000px) and ensures faster processing. To preserve full resolution up to 4096px, enable hd_mode: true (2 credits). You can also set a specific target with max_dimension (512–4096).
Supported Input Formats
GIF, BMP, TIFF, and SVG are not supported. Unsupported formats return a 400 UNSUPPORTED_FORMAT error.
Response (201 Created)
{
"job": {
"id": "a6b60315-e26c-4a8c-8100-82c3cc9cfcb9",
"status": "running",
"output_mode": "download_url",
"created_at": "2025-12-22T09:22:44.393Z"
},
"message": "Job created successfully. Poll GET /api/v1/jobs/{id} for status.",
"request_id": "req_1703260800000_a1b2c3d4e5f6"
}/jobs/:idPath Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Job ID returned from create job |
Example Request
curl https://api.simplypng.app/api/v1/jobs/a6b60315-e26c-4a8c-8100-82c3cc9cfcb9 \ -H "Authorization: Bearer sp_test_YOUR_API_KEY"
Response - Running
{
"job": {
"id": "a6b60315-e26c-4a8c-8100-82c3cc9cfcb9",
"status": "running",
"output_mode": "download_url",
"credits_charged": 0,
"created_at": "2025-12-22T09:22:44.393Z"
},
"request_id": "req_1703260801000_b2c3d4e5f6a7"
}Response - Succeeded (download_url)
{
"job": {
"id": "a6b60315-e26c-4a8c-8100-82c3cc9cfcb9",
"status": "succeeded",
"output_mode": "download_url",
"credits_charged": 1,
"created_at": "2025-12-22T09:22:44.393Z",
"result": {
"type": "url",
"url": "https://storage.simplypng.app/output/abc123.png",
"thumbnail_url": "https://storage.simplypng.app/output/abc123_thumb.png",
"expires_at": "2025-12-22T10:22:44.393Z"
}
},
"request_id": "req_1703260802000_c3d4e5f6a7b8"
}Response - Succeeded (base64_json)
{
"job": {
"id": "a6b60315-e26c-4a8c-8100-82c3cc9cfcb9",
"status": "succeeded",
"output_mode": "base64_json",
"credits_charged": 1,
"created_at": "2025-12-22T09:22:44.393Z",
"result": {
"type": "base64",
"data": "iVBORw0KGgoAAAANSUhEUgAA...",
"format": "png"
}
},
"request_id": "req_1703260802000_c3d4e5f6a7b8"
}Response - Failed
{
"job": {
"id": "a6b60315-e26c-4a8c-8100-82c3cc9cfcb9",
"status": "failed",
"output_mode": "download_url",
"credits_charged": 0,
"created_at": "2025-12-22T09:22:44.393Z",
"error": {
"code": "PROCESSING_FAILED",
"message": "Image processing failed"
}
},
"request_id": "req_1703260802000_c3d4e5f6a7b8"
}/jobs/batchRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
images | array | Yes | Array of image objects. Each must have url or base64. Optional id for client correlation. |
output_mode | string | No | download_url (default) or base64_json |
webhook_url | string | No | HTTPS URL to receive a POST notification when the batch completes. A webhook_secret is returned in the response for signature verification. |
idempotency_key | string | No | Unique key to prevent duplicate batches (max 255 chars). |
options | object | No | Same processing options as single job (hd_mode, output_type, background, etc.). Applied to all images. |
Batch Limits by Plan
| Plan | Max Images | Max Total Size |
|---|---|---|
| PayG / Basic | 10 | 250 MB |
| Plus | 25 | 500 MB |
| Pro / Creator / Studio | 50 | Up to 10 GB |
Example Request
curl -X POST https://api.simplypng.app/api/v1/jobs/batch \
-H "Authorization: Bearer sp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [
{"url": "https://your-storage.com/photo1.jpg", "id": "product-001"},
{"url": "https://your-storage.com/photo2.jpg", "id": "product-002"}
],
"options": {
"output_type": "centered",
"background": "white",
"output_format": "jpg"
}
}'Response (202 Accepted)
{
"batch": {
"id": "batch_abc123",
"status": "processing",
"total_count": 2
},
"jobs": [
{"id": "job_1", "status": "running", "index": 0},
{"id": "job_2", "status": "running", "index": 1}
],
"request_id": "req_..."
}Webhook Notifications
When you provide a webhook_url, the response includes a webhook_secret in the batch object. Store this secret securely — it is only returned once. When the batch completes, SimplyPNG sends a POST request to your URL with HMAC-SHA256 signature verification.
Webhook Headers
| Header | Description |
|---|---|
X-SimplyPNG-Signature | sha256=<hmac_hex> |
X-SimplyPNG-Timestamp | Unix timestamp (seconds) — reject if older than 5 minutes |
Webhook Payload
{
"event": "batch.completed",
"batch_id": "batch_abc123",
"status": "completed",
"total": 50,
"succeeded": 48,
"failed": 2,
"created_at": "2026-02-16T...",
"completed_at": "2026-02-16T..."
}Event types: batch.completed, batch.partial, batch.failed
Verifying Signatures (Python)
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
# Reject if older than 5 minutes (replay protection)
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{raw_body.decode()}"
expected = "sha256=" + hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)/jobs/batch/:idResponse — Completed
{
"batch": {
"id": "batch_abc123",
"status": "completed",
"total_count": 2
},
"results": [
{
"index": 0,
"job_id": "job_1",
"client_id": "product-001",
"status": "succeeded",
"credits_charged": 1,
"result": {
"url": "https://storage.simplypng.app/...",
"thumbnail_url": "https://storage.simplypng.app/...",
"expires_at": "2026-02-09T..."
}
}
],
"timing": {
"total_seconds": 16,
"avg_per_image": 8
},
"request_id": "req_..."
}Results may be returned in completion order, not submission order. Use client_id to correlate results with your input images.
Workflow
- Split your images into batches of up to 50
- Submit batches with controlled concurrency (3–5 in parallel)
- Poll each batch status via
GET /jobs/batch/:id - Download results as each batch completes (1-hour download window)
Time Estimates (1,000 images, standard mode)
| Strategy | Batches | Est. Wall-Clock Time | Credits |
|---|---|---|---|
| Sequential (1 at a time) | 20 | ~28 min | 1,000 |
| 3 concurrent | 20 | ~10 min | 1,000 |
| 5 concurrent | 20 | ~6 min | 1,000 |
Estimates assume ~1.4s/image processing + 15s cold start per batch. Actual times depend on image size and GPU availability. HD mode (~1.7s/image) uses 2 credits per image.
Python Example
import requests
import time
import concurrent.futures
API_KEY = "sp_live_YOUR_KEY"
BASE = "https://api.simplypng.app/api/v1"
BATCH_SIZE = 50
MAX_CONCURRENT = 3
def submit_batch(image_urls):
"""Submit a batch of up to 50 image URLs."""
resp = requests.post(
f"{BASE}/jobs/batch",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"images": [{"url": u} for u in image_urls],
"output_mode": "download_url",
},
)
resp.raise_for_status()
return resp.json()["batch"]["id"]
def poll_batch(batch_id):
"""Poll until batch completes."""
while True:
resp = requests.get(
f"{BASE}/jobs/batch/{batch_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
data = resp.json()
status = data["batch"]["status"]
if status in ("completed", "partial", "failed"):
return data
time.sleep(5)
# 1. Split into batches of 50
all_urls = ["https://example.com/img1.jpg", ...] # your 1000 URLs
batches = [
all_urls[i : i + BATCH_SIZE]
for i in range(0, len(all_urls), BATCH_SIZE)
]
# 2. Submit with controlled concurrency
with concurrent.futures.ThreadPoolExecutor(
max_workers=MAX_CONCURRENT
) as pool:
batch_ids = list(pool.map(submit_batch, batches))
print(f"Submitted {len(batch_ids)} batches")
# 3. Poll and download results
for batch_id in batch_ids:
result = poll_batch(batch_id)
succeeded = result["batch"]["succeeded"]
total = result["batch"]["total"]
print(f"Batch {batch_id}: {succeeded}/{total} succeeded")
for r in result["results"]:
if r["status"] == "succeeded":
url = r["result"]["url"]
# Download: requests.get(url, headers=...)
Node.js Example
const API_KEY = "sp_live_YOUR_KEY";
const BASE = "https://api.simplypng.app/api/v1";
const BATCH_SIZE = 50;
const MAX_CONCURRENT = 3;
async function submitBatch(imageUrls) {
const resp = await fetch(`${BASE}/jobs/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
images: imageUrls.map((url) => ({ url })),
output_mode: "download_url",
}),
});
const data = await resp.json();
return data.batch.id;
}
async function pollBatch(batchId) {
while (true) {
const resp = await fetch(`${BASE}/jobs/batch/${batchId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const data = await resp.json();
if (["completed", "partial", "failed"].includes(data.batch.status)) {
return data;
}
await new Promise((r) => setTimeout(r, 5000));
}
}
async function processAll(allUrls) {
// 1. Split into batches of 50
const batches = [];
for (let i = 0; i < allUrls.length; i += BATCH_SIZE) {
batches.push(allUrls.slice(i, i + BATCH_SIZE));
}
// 2. Submit with controlled concurrency
const batchIds = [];
for (let i = 0; i < batches.length; i += MAX_CONCURRENT) {
const chunk = batches.slice(i, i + MAX_CONCURRENT);
const ids = await Promise.all(chunk.map(submitBatch));
batchIds.push(...ids);
}
console.log(`Submitted ${batchIds.length} batches`);
// 3. Poll and collect results
for (const id of batchIds) {
const result = await pollBatch(id);
const { succeeded, total } = result.batch;
console.log(`Batch ${id}: ${succeeded}/${total} succeeded`);
}
}
Tips
- Use
idempotency_keyper batch to safely retry on network failures - Download results promptly — output URLs expire after 1 hour
- Use URL input (not base64) for large images to avoid payload limits
- For HD mode, set
options.hd_mode: true(2 credits per image) - Monitor your credit balance via
GET /credits/balancebefore large runs
Job Statuses
| Status | Description |
|---|---|
pending | Job is queued and waiting to be processed |
running | Job is currently being processed |
succeeded | Job completed successfully, result available |
failed | Job failed, check error for details |
Error Response Format
All errors follow this format:
{
"error": {
"type": "error_type",
"code": "ERROR_CODE",
"message": "Human-readable error message",
"param": "field_name" // optional, for validation errors
},
"request_id": "req_1703260800000_a1b2c3d4e5f6"
}Error Types
| Type | HTTP Status | Description |
|---|---|---|
validation_error | 400 | Invalid request parameters |
authentication_error | 401 | Missing or invalid API key |
billing_error | 402/403 | Insufficient credits or inactive plan |
not_found_error | 404 | Resource not found |
conflict | 409 | Duplicate idempotency key in progress |
rate_limit_error | 429 | Too many requests |
internal_error | 500 | Server error, retry later |
Validation Error Codes
When request parameters are invalid, the API returns 400 with a specific error code:
| Code | Trigger |
|---|---|
INVALID_OUTPUT_TYPE | Invalid output_type value (valid: original, centered) |
INVALID_BACKGROUND | Invalid background value (valid: transparent, white, custom) |
INVALID_BACKGROUND_COLOR | Invalid hex color format (must be #RRGGBB or #RGB) |
MISSING_BACKGROUND_COLOR | background is custom but background_color is missing |
INVALID_OUTPUT_FORMAT | Invalid output_format value (valid: png, jpg) |
INVALID_CANVAS_PRESET | Invalid canvas_size_preset value |
INVALID_OUTPUT_MODE | Invalid output_mode value (valid: download_url, base64_json) |
INVALID_WEBHOOK_URL | Invalid or non-HTTPS webhook_url (batch endpoint only) |
SSRF_BLOCKED | Image URL points to a private or internal address |
UNSUPPORTED_FORMAT | Image format not supported (GIF, BMP, TIFF, SVG) |
Auto-Correction Behaviors
Some conflicting option combinations are automatically resolved instead of rejected:
| Conflict | Resolution |
|---|---|
centered + transparent | Background changed to white |
jpg + transparent | Background changed to white (JPEG format preserved) |
Idempotency
To prevent duplicate jobs from network retries, include an idempotency key in your request. If a job with the same key already exists, the API will return the existing job instead of creating a new one. Keys are scoped per owner (user or organization) and can be up to 255 characters long.
You can provide the key via either method (header takes priority):
- HTTP Header (recommended):
Idempotency-Key: your-unique-key - Request body:
"idempotency_key": "your-unique-key"
curl -X POST https://api.simplypng.app/api/v1/jobs \
-H "Authorization: Bearer sp_live_YOUR_KEY" \
-H "Idempotency-Key: user-123-image-456-v1" \
-H "Content-Type: application/json" \
-d '{"image": "https://your-storage.com/photo.jpg"}'The response will include "idempotent": true if returning an existing job. If another request with the same key is currently being processed, you will receive a 409 Conflict response.
Request IDs
Every API response includes a request_id. Include this ID when contacting support about specific requests. Request IDs are also returned in the X-Request-ID header.