PinPic API
Create, manage, and embed interactive images programmatically. Generate AI visuals, auto-detect hotspots, and turn whole articles into interactive content with a single call.
Authentication
Every request requires an API key, which you can create on the API Keys page of your dashboard. Keys use the format pk_ followed by 48 hex characters and can be supplied three ways:
| Method | Example |
|---|---|
| Authorization header (preferred) | Authorization: Bearer pk_... |
| Dedicated header | X-API-Key: pk_... |
| Query parameter | ?api_key=pk_... |
The API is available on premium plans. A free-plan key authenticates, but endpoints return 403 until the account is upgraded. Free accounts can still use AI features inside the dashboard.
Response envelopes
Successful responses return { "success": true, "data": { ... } }. Errors return { "success": false, "error": "message", "code": <int> }, where code matches the HTTP status.
CORS & identifiers
All endpoints allow cross-origin requests. Endpoints with an {id} parameter accept either the numeric ID or the URL-safe unique_id (for example a1b2c3d4) — prefer the unique_id in stored URLs.
Limits & Credits
Hourly rate limit
Each key has a rolling-hour request ceiling (default 1000). Exceeding it returns 429 Rate limit exceeded. Every request counts toward the limit regardless of outcome.
Monthly AI credits
Each AI operation costs a fixed number of credits from a monthly pool that resets at the start of each calendar month (UTC). Credits are claimed before the work runs; failed operations release their credits, so a transient error never wastes quota.
| Plan | Monthly credits |
|---|---|
| Free | 10 |
| Pro | 100 |
| Agency | 2,000 |
Image generation cost
Image cost depends on a model tier × quality matrix. The model field on the generation endpoints picks the tier; omitting it defaults to ultra.
| Model tier | low | medium | high | auto* |
|---|---|---|---|---|
mini | 1 | 3 | 5 | 3 |
standard | 1 | 6 | 12 | 6 |
plus | 1 | 8 | 16 | 8 |
ultra (default) | 1 | 10 | 20 | 10 |
* auto quality is billed at the medium rate.
Other AI operations each cost 1 credit: hotspot suggestion, concept suggestion, concept expansion, detailed-prompt generation, and locating image spots in an article. Bundled pipelines charge per phase as the work runs — expansion (1, if a concept is supplied), the image (per the matrix above), and hotspots (1, if enabled).
Image limit
Creating an image returns 403 Image limit reached once your plan's image cap is hit. Check your account page for the current value.
Image Management
/api/imagesList images, or bulk-look-up specific ones. Without parameters it returns a paginated list of every image owned by the key.
| Param | Type | Notes |
|---|---|---|
page | int | Default 1. |
limit | int | Default 20, clamped to 1–100. |
cursor | string | Opaque cursor from a previous response's next_cursor. Constant-cost regardless of depth — preferred for large collections. |
ids | string | Comma-separated IDs for bulk lookup (max 100). Returns records in the requested order; unknown IDs are listed under missing. |
Each image record looks like:
{
"id": 1,
"unique_id": "a1b2c3d4",
"title": "Product Demo",
"description": "...",
"category": "Product",
"image_url": "https://files.pinpic.app/A1b2c3d4e5f6g7h8_2000.webp",
"share_url": "https://pinpic.app/share/images/a1b2c3d4",
"embed_url": "https://dash.pinpic.app/images/a1b2c3d4/embed",
"hotspots": [
{ "x": 25.5, "y": 30.2, "title": "Feature 1",
"description": "Click to learn more", "color": "#2a9d8f",
"link": "https://example.com", "link_label": "Learn More" }
],
"created_at": "2026-01-15 10:30:00",
"updated_at": "2026-01-15 10:30:00"
}
/api/images/{id}Returns a single image record. Responds 404 if it isn't found or isn't owned by the key.
/api/imagesCreate an interactive image. Supply one of image_url or image_data.
| Field | Type | Notes |
|---|---|---|
image_url | string | HTTPS URL fetched server-side (~25 MB cap, 30s timeout). |
image_data | string | Base64 image data (~25 MB decoded cap). |
title | string | Optional. Defaults to "API Image". |
description | string | Optional. |
hotspots | array | Optional. Each: x (0–100), y (0–100), title, description, color, link, link_label, video_url. |
Returns 201 with the new record.
/api/images/{id}Update title, description, or hotspots (which replaces the existing set). At least one field is required.
/api/images/{id}Removes the image and its stored files. Only the account owner may delete; workspace contributors receive 403.
/api/images/{id}/generate-hotspotsSuggests hotspots for an image without saving them — pass the result through PUT /api/images/{id} to persist. Costs 1 credit.
| Field | Type | Notes |
|---|---|---|
prompt | string | Optional. Synthesized from title + description if omitted. |
max_hotspots | int | Optional. Default 8, clamped 1–20. |
current_hotspots | array | Optional seed for iterative edits. |
AI Generation
Two patterns are supported. Composable sync calls each step yourself — best when you want a human in the loop. The background pipeline returns an embed code immediately and does the generation work in the background — best for batch processing or dropping an embed into a page without waiting.
/api/images/ai/locate-image-spotsAnalyzes article text and proposes N image placements. Costs 1 credit. Requires page_content; optional headings (anchor text) and count (1–10, default 3).
/api/images/ai/suggest-conceptsProposes 2–5 concept directions, each { id, title, summary }. Costs 1 credit. Provide at least one of page_content, section_context, or purpose.
/api/images/ai/expand-conceptExpands a concept into a detailed image prompt and suggested aspect ratio. Costs 1 credit.
/api/images/ai/generate-detailed-promptOne-shot prompt from a purpose string when you have no article context. Costs 1 credit.
/api/images/ai/generate-imageGenerates an image synchronously (blocks ~10–30s) and uploads it to PinPic Cloud. Costs credits per the model × quality matrix; failed generations release the credits.
| Field | Type | Notes |
|---|---|---|
prompt | string | Required. Detailed prompt. |
model | string | Optional tier: mini, standard, plus, ultra (default). Unknown values return 422. |
aspect_ratio | string | Optional. auto, 1:1, 16:9, 9:16, 4:3, 3:4, and more. Default 1:1. |
quality | string | Optional. low, medium, high, auto. Default low. |
This returns the image asset only. Follow up with POST /api/images using the returned image_url to create an interactive-image record.
/api/images/ai/processMints an image record and returns its embed code immediately (typically <200ms), then generates the image in the background. The embed lights up on its own once the work finishes — no polling required. Provide either prompt or concept.
{
"success": true,
"data": {
"job_id": 43,
"status": "queued",
"unique_id": "1e726bdb",
"embed_url": "https://dash.pinpic.app/images/1e726bdb/embed",
"share_url": "https://pinpic.app/share/images/1e726bdb",
"embed_code": "..."
}
}
Optional fields include page_content and section_context (context for concept expansion), quality, model, aspect_ratio, detect_hotspots (default true), and hotspot_prompt.
/api/articles/processEnd-to-end batch pipeline: locate spots → expand → generate → create record → suggest hotspots, for N images. Returns 202 with a job_id. Requires page_content; optional headings, count (1–10, default 3), quality, and model.
/api/articles/process/{job_id}Polls the status of an article or single-image job (recommended interval 2–5s). Status values: queued, running, completed, failed. Completed jobs include a results array with each image's unique_id, image_url, and hotspots.
Embedding
/api/images/{id}/embedReturns ready-to-paste embed code: a share URL, a JavaScript snippet, and an iframe snippet. Generates a unique_id if the image doesn't have one yet.
/api/images/{id}/embed-dataThe render-time JSON payload used by the embed snippet. No API key required — access is gated by the owner's embed key supplied via the user parameter. The {id} here must be the unique_id.
| Param | Type | Notes |
|---|---|---|
user | string | Required. The image owner's embed key. |
variant | string | Optional width: 256, 512, 720, 1024, 1500, 2000, or aliases (thumbnail, small, card, medium, large, xlarge). |
The payload includes the full hotspot list, tooltip and label styling, attribution metadata, SEO fields, and auto-open behavior. Responses are cached aggressively and support conditional GETs.
Image variants
Every accepted image is delivered through PinPic Cloud and resized into a standard set of widths (256 / 512 / 720 / 1024 / 1500 / 2000, all WebP). Swap the trailing _<width> token in the URL to request a different size, for example ..._512.webp.
Status Codes
| Code | Meaning |
|---|---|
200 | Success |
201 | Image created |
202 | Async job enqueued (body contains job_id) |
400 | Missing or malformed field |
401 | API key missing or invalid |
403 | Premium required, plan-locked feature, image limit, or wrong embed key |
404 | Not found or not owned by key |
405 | Wrong HTTP method |
422 | AI validation failure (missing context, bad aspect ratio) |
429 | Hourly rate limit or monthly AI quota exhausted |
500 | Server error |
503 | AI quota subsystem unreachable |
Code Examples
JavaScript (Fetch)
const headers = {
'Authorization': 'Bearer YOUR_PK',
'Content-Type': 'application/json',
};
// List images
const list = await fetch('https://dash.pinpic.app/api/images?page=1&limit=20', { headers })
.then(r => r.json());
// Create from a URL
const created = await fetch('https://dash.pinpic.app/api/images', {
method: 'POST',
headers,
body: JSON.stringify({
title: 'My Interactive Image',
image_url: 'https://example.com/image.jpg',
hotspots: [
{ x: 25.5, y: 30.2, title: 'Feature 1', description: 'Click to learn more' }
],
}),
}).then(r => r.json());
Python (requests)
import requests, time
API = "https://dash.pinpic.app/api"
HEADERS = {"Authorization": "Bearer YOUR_PK", "Content-Type": "application/json"}
# Create an image
created = requests.post(f"{API}/images", headers=HEADERS, json={
"title": "My Image",
"image_url": "https://example.com/image.jpg",
}).json()
# Process a whole article
job = requests.post(f"{API}/articles/process", headers=HEADERS, json={
"page_content": article_text,
"count": 3,
}).json()
job_id = job["data"]["job_id"]
while True:
status = requests.get(f"{API}/articles/process/{job_id}", headers=HEADERS).json()
if status["data"]["status"] in ("completed", "failed"):
break
time.sleep(3)
PHP (cURL)
$apiKey = 'YOUR_PK';
$ch = curl_init('https://dash.pinpic.app/api/images');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'title' => 'Sample',
'image_url' => 'https://example.com/image.jpg',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);