PaperBanana API Docs
Generate methodology diagrams, optimize illustration prompts, create statistical plots, and compose figures through a task-based async API.
English
Overview
PaperBanana is an AI-powered API for generating publication-ready academic visuals. It currently supports five workflows:
- Methodology diagrams for architectures, pipelines, and research workflows
- PDF-to-methodology diagrams for extracting selected paper pages before generation
- Illustration prompt optimization for polishing diagram or plot prompts before generation
- Statistical plots for bar charts, line plots, histograms, scatter plots, and similar figures
- Composite figures for stitching multiple completed task outputs into a labeled multi-panel figure
The API follows an async task workflow: submit a request, receive a task_id,
then poll until the result is ready. Most jobs finish in 30 to 120 seconds depending on complexity and iteration count.
Base URL
https://api.paperbanana.me
All endpoint paths in this document are relative to that base URL.
Authentication
All generation and admin endpoints require a Bearer token in the
Authorization header:
Authorization: Bearer pb_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
| Token Type | Scope | How It Is Created |
|---|---|---|
API Key |
/api/generate, /api/generate-from-pdf, /api/optimize-prompt, /api/plot, /api/composite, /api/tasks/* |
Created by an admin through POST /api/admin/keys or the admin panel |
Admin Key |
/api/admin/* |
Configured through the PAPERBANANA_ADMIN_KEY environment variable |
generate, generate-from-pdf, optimize-prompt, plot, or composite submission consumes one unit
as soon as the task is accepted.
Typical Workflow
A standard integration follows three steps:
1. POST /api/generate (or /api/generate-from-pdf, /api/optimize-prompt, /api/plot, or /api/composite) -> { task_id, status: "pending" }
2. GET /api/tasks/{task_id} -> { status: "running" }
3. GET /api/tasks/{task_id} -> { status: "completed", description: "..." }
Optional:
GET /api/tasks/{task_id}/image -> download the PNG directly for image-producing tasks
GET /api/tasks/{task_id}/image?format=svg -> download SVG when a plot task exported it
Error Handling
| Status | Meaning | Typical Cause |
|---|---|---|
401 | Unauthorized | Missing or invalid API key / admin key |
403 | Forbidden | API key disabled or quota exhausted |
404 | Not found | Unknown task ID, missing key, or inaccessible image |
422 | Validation error | Missing required fields or invalid JSON in data_json |
500 | Server error | Internal configuration or runtime issue |
Error responses use a JSON payload:
{
"detail": "Human-readable error message"
}
Health Check
GET/api/health
Use this endpoint to verify that the API is online. No authentication is required.
Response 200
{
"status": "ok",
"version": "0.1.2"
}
curl https://api.paperbanana.me/api/health
const res = await fetch("https://api.paperbanana.me/api/health");
const data = await res.json();
console.log(data);
import requests
r = requests.get("https://api.paperbanana.me/api/health")
print(r.json())
Generate Methodology Diagram
POST/api/generate
Submit methodology text and a figure caption to create an academic-style architecture diagram, workflow diagram, or system overview. The endpoint returns a task immediately.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
source_context | string | Yes | Methodology text or a relevant excerpt from the paper |
caption | string | Yes | Figure title, for example Figure 1: Transformer encoder architecture |
iterations | integer | No | Refinement iterations from 1 to 10. Default is 3 |
aspect_ratio | string | No | Optional target aspect ratio. Supported values: auto, 1:1, 5:4, 2:3, 3:2, 3:4, 4:5, 4:3, 9:16, 16:9, 21:9. If omitted, the server keeps its current default layout |
output_formats | array[string] | No | Optional methodology diagram output formats. Supported value: png. PNG preview remains available for polling responses |
vlm_provider | string | No | Optional VLM provider override. Supported values: gemini, openrouter, kie, apimart. If omitted, the server keeps its configured default provider |
vlm_model | string | No | Optional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model |
image_model | string | No | Optional Kie image model override. Supported values: nano-banana-pro, google/nano-banana, nano-banana-2, gpt-image-2-text-to-image. Only works when the server image provider is kie_imagen |
image_resolution | string | No | Optional Kie image resolution override. Supported values: 1K, 2K, 4K. Supported by nano-banana-pro, nano-banana-2, and gpt-image-2-text-to-image; google/nano-banana does not accept it. For GPT Image 2, aspect_ratio: "auto" only supports 1K, and 1:1 does not support 4K |
webhook_url | string | No | Optional callback endpoint to receive the final result |
webhook_include_image | boolean | No | When true, include image_base64 in the webhook payload |
Response 202 Accepted
{
"task_id": "a1b2c3d4e5f6...",
"status": "pending"
}
{
"vlm_provider": "kie",
"vlm_model": "gemini-2.5-flash"
}
To route requests through APIMart:
{
"vlm_provider": "apimart",
"vlm_model": "gemini-2.5-flash"
}
{
"image_model": "gpt-image-2-text-to-image",
"aspect_ratio": "16:9",
"image_resolution": "4K"
}
PaperBanana sends GPT Image 2's native aspect_ratio and resolution fields to Kie. Use 1K when aspect_ratio is auto.
curl -X POST https://api.paperbanana.me/api/generate \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"source_context": "Our model uses a 6-layer Transformer encoder with multi-head self-attention.",
"caption": "Figure 1: Transformer encoder architecture",
"iterations": 3,
"aspect_ratio": "16:9",
"output_formats": ["png"],
"vlm_provider": "kie",
"vlm_model": "gemini-2.5-flash",
"image_model": "nano-banana-pro",
"image_resolution": "4K"
}'
const API_KEY = "pb_your_api_key_here";
const BASE = "https://api.paperbanana.me";
const submitRes = await fetch(`${BASE}/api/generate`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
source_context: "Our model uses a 6-layer Transformer encoder...",
caption: "Figure 1: Transformer encoder architecture",
iterations: 3,
aspect_ratio: "16:9",
output_formats: ["png"],
vlm_provider: "kie",
vlm_model: "gemini-2.5-flash",
image_model: "nano-banana-pro",
image_resolution: "4K"
})
});
const { task_id } = await submitRes.json();
while (true) {
const res = await fetch(`${BASE}/api/tasks/${task_id}`, {
headers: { "Authorization": `Bearer ${API_KEY}` }
});
const task = await res.json();
if (task.status === "completed" || task.status === "failed") {
console.log(task);
break;
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
import time
import requests
API_KEY = "pb_your_api_key_here"
BASE = "https://api.paperbanana.me"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
r = requests.post(f"{BASE}/api/generate", headers=HEADERS, json={
"source_context": "Our model uses a 6-layer Transformer encoder...",
"caption": "Figure 1: Transformer encoder architecture",
"iterations": 3,
"aspect_ratio": "16:9",
"output_formats": ["png"],
"vlm_provider": "kie",
"vlm_model": "gemini-2.5-flash",
"image_model": "nano-banana-pro",
"image_resolution": "4K",
})
r.raise_for_status()
task_id = r.json()["task_id"]
while True:
task = requests.get(f"{BASE}/api/tasks/{task_id}", headers=HEADERS).json()
if task["status"] in {"completed", "failed"}:
print(task)
break
time.sleep(5)
Generate From PDF
POST/api/generate-from-pdf
Submit a base64-encoded PDF and a figure caption. The server extracts text from all pages or
from the selected page range, then runs the same methodology diagram pipeline used by
/api/generate.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
pdf_base64 | string | Yes | Base64-encoded PDF content. A data:application/pdf;base64,... data URL is also accepted |
caption | string | Yes | Figure title or communicative intent for the generated methodology diagram |
pages | string | No | Optional 1-based page selection, for example 1, 1,3-5, or 5-3. If omitted, all pages are used |
iterations | integer | No | Refinement iterations from 1 to 10. Default is 3 |
aspect_ratio | string | No | Optional target aspect ratio. Same values as /api/generate |
output_formats | array[string] | No | Optional methodology diagram output formats. Supported value: png |
vlm_provider | string | No | Optional VLM provider override. Supported values: gemini, openrouter, kie, apimart |
vlm_model | string | No | Optional VLM model override |
image_model | string | No | Optional Kie image model override |
image_resolution | string | No | Optional Kie image resolution override |
webhook_url | string | No | Optional callback endpoint to receive the final result |
webhook_include_image | boolean | No | When true, include image_base64 in the webhook payload |
Response 202 Accepted
{
"task_id": "a1b2c3d4e5f6...",
"status": "pending"
}
PDF_B64=$(base64 -i paper.pdf | tr -d '\n')
curl -X POST https://api.paperbanana.me/api/generate-from-pdf \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d "{
\"pdf_base64\": \"${PDF_B64}\",
\"caption\": \"Figure 1: Overall methodology pipeline\",
\"pages\": \"2-4\",
\"iterations\": 3,
\"aspect_ratio\": \"16:9\",
\"output_formats\": [\"png\"]
}"
const file = document.querySelector("input[type=file]").files[0];
const pdf_base64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result).split(",", 2)[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const res = await fetch("https://api.paperbanana.me/api/generate-from-pdf", {
method: "POST",
headers: {
"Authorization": "Bearer pb_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
pdf_base64,
caption: "Figure 1: Overall methodology pipeline",
pages: "2-4",
output_formats: ["png"]
})
});
const { task_id } = await res.json();
import base64
import requests
API_KEY = "pb_your_api_key_here"
BASE = "https://api.paperbanana.me"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
pdf_base64 = base64.b64encode(open("paper.pdf", "rb").read()).decode("ascii")
r = requests.post(f"{BASE}/api/generate-from-pdf", headers=HEADERS, json={
"pdf_base64": pdf_base64,
"caption": "Figure 1: Overall methodology pipeline",
"pages": "2-4",
"output_formats": ["png"],
})
r.raise_for_status()
print(r.json())
Optimize Illustration Prompt
POST/api/optimize-prompt
Submit an existing diagram or plot prompt and let the Stylist stage polish it into a clearer, more publication-ready academic illustration description. This is a text-only task and does not render an image.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | The prompt or detailed description to optimize |
source_context | string | No | Optional paper excerpt or methodology context used to keep the rewrite grounded |
caption | string | No | Optional figure caption or communicative intent |
diagram_type | string | No | Target output type for the optimized prompt. Supported values: methodology, statistical_plot. Default is methodology |
guidelines | string | No | Optional custom style guidelines. When omitted, the server uses its built-in methodology or plot guideline set |
vlm_provider | string | No | Optional VLM provider override. Supported values: gemini, openrouter, kie, apimart |
vlm_model | string | No | Optional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model |
webhook_url | string | No | Optional callback endpoint to receive the final result |
webhook_include_image | boolean | No | Accepted for API consistency, but ignored because this task does not generate images |
Response 202 Accepted
{
"task_id": "b7c8d9e0f1a2...",
"status": "pending"
}
description.
image_base64 and artifacts remain empty for this endpoint.
curl -X POST https://api.paperbanana.me/api/optimize-prompt \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"description": "Draw a transformer encoder with token embeddings, positional encoding, 6 encoder blocks, and a classifier head.",
"source_context": "Our model uses a 6-layer Transformer encoder with multi-head self-attention.",
"caption": "Figure 1: Transformer encoder architecture",
"diagram_type": "methodology"
}'
const res = await fetch("https://api.paperbanana.me/api/optimize-prompt", {
method: "POST",
headers: {
"Authorization": "Bearer pb_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
description: "Draw a transformer encoder with token embeddings...",
source_context: "Our model uses a 6-layer Transformer encoder...",
caption: "Figure 1: Transformer encoder architecture",
diagram_type: "methodology"
})
});
const { task_id } = await res.json();
console.log(task_id);
import requests
r = requests.post("https://api.paperbanana.me/api/optimize-prompt", headers=HEADERS, json={
"description": "Draw a transformer encoder with token embeddings...",
"source_context": "Our model uses a 6-layer Transformer encoder...",
"caption": "Figure 1: Transformer encoder architecture",
"diagram_type": "methodology",
})
print(r.json())
Generate Statistical Plot
POST/api/plot
Submit structured data and an intent description to generate a publication-style chart such as a bar chart, line plot, histogram, or scatter plot.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
data_json | string | Yes | A JSON string containing the plotting data |
intent | string | Yes | Description of the desired chart, axes, and styling expectations |
iterations | integer | No | Refinement iterations from 1 to 10. Default is 3 |
aspect_ratio | string | No | Optional target canvas ratio. Supported values: auto, 1:1, 5:4, 2:3, 3:2, 3:4, 4:5, 4:3, 9:16, 16:9, 21:9. The server uses it to steer the generated figure layout |
output_formats | array[string] | No | Optional output formats for plots. Supported values: png, svg, pdf. PNG preview remains available for polling responses |
vlm_provider | string | No | Optional VLM provider override. Supported values: gemini, openrouter, kie, apimart. If omitted, the server keeps its configured default provider |
vlm_model | string | No | Optional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model |
webhook_url | string | No | Optional callback endpoint to receive the final result |
webhook_include_image | boolean | No | When true, include image_base64 in the webhook payload |
Response 202 Accepted
{
"task_id": "f7e8d9c0b1a2...",
"status": "pending"
}
{
"vlm_provider": "kie",
"vlm_model": "gemini-2.5-flash"
}
To route requests through APIMart:
{
"vlm_provider": "apimart",
"vlm_model": "gemini-2.5-flash"
}
data_json
This field must be a JSON string, not a nested JSON object.
Use json.dumps(data) in Python or JSON.stringify(data) in JavaScript before sending it.
curl -X POST https://api.paperbanana.me/api/plot \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"data_json": "{\"models\":[\"ResNet-50\",\"ViT-B/16\",\"EfficientNet-B4\"],\"accuracy\":[76.1,81.8,82.9]}",
"intent": "Bar chart comparing model accuracy with value labels",
"iterations": 3,
"aspect_ratio": "4:3",
"output_formats": ["png", "svg", "pdf"],
"vlm_provider": "apimart",
"vlm_model": "gemini-2.5-flash"
}'
const data = {
models: ["ResNet-50", "ViT-B/16", "EfficientNet-B4"],
accuracy: [76.1, 81.8, 82.9]
};
const res = await fetch("https://api.paperbanana.me/api/plot", {
method: "POST",
headers: {
"Authorization": "Bearer pb_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
data_json: JSON.stringify(data),
intent: "Bar chart comparing model accuracy with value labels",
iterations: 3,
aspect_ratio: "4:3",
output_formats: ["png", "svg", "pdf"],
vlm_provider: "apimart",
vlm_model: "gemini-2.5-flash"
})
});
console.log(await res.json());
import json
import requests
data = {
"models": ["ResNet-50", "ViT-B/16", "EfficientNet-B4"],
"accuracy": [76.1, 81.8, 82.9],
}
r = requests.post("https://api.paperbanana.me/api/plot", headers=HEADERS, json={
"data_json": json.dumps(data),
"intent": "Bar chart comparing model accuracy with value labels",
"iterations": 3,
"aspect_ratio": "4:3",
"output_formats": ["png", "svg", "pdf"],
"vlm_provider": "apimart",
"vlm_model": "gemini-2.5-flash",
})
print(r.json())
Compose Multi-Panel Figure
POST/api/composite
Compose multiple completed task outputs into a single PNG figure with optional panel labels. This endpoint performs local image processing on the server and does not trigger a new upstream model call.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
source_task_ids | array[string] | Yes | At least two completed task IDs owned by the same API key |
layout | string | No | Grid layout as RxC such as 1x3 or 2x2, or auto. Default is auto |
labels | array[string] | No | Optional explicit panel labels. Omit to auto-generate (a), (b), (c), ... |
spacing | integer | No | Spacing in pixels between panels and around the outer margin. Default is 20 |
label_position | string | No | Where labels are rendered relative to each panel. Supported values: top, bottom. Default is bottom |
label_font_size | integer | No | Font size for panel labels. Default is 32 |
webhook_url | string | No | Optional callback endpoint to receive the final result |
webhook_include_image | boolean | No | When true, include image_base64 in the webhook payload |
Response 202 Accepted
{
"task_id": "c0ffee123456...",
"status": "pending"
}
source_task_ids must belong to the same API key, must already be
completed, and must still have an image file available on disk.
curl -X POST https://api.paperbanana.me/api/composite \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"source_task_ids": ["task_a", "task_b", "task_c"],
"layout": "1x3",
"labels": ["(a)", "(b)", "(c)"],
"spacing": 20,
"label_position": "bottom",
"label_font_size": 32
}'
const res = await fetch("https://api.paperbanana.me/api/composite", {
method: "POST",
headers: {
"Authorization": "Bearer pb_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
source_task_ids: ["task_a", "task_b", "task_c"],
layout: "1x3",
labels: ["(a)", "(b)", "(c)"],
spacing: 20,
label_position: "bottom",
label_font_size: 32
})
});
console.log(await res.json());
import requests
r = requests.post("https://api.paperbanana.me/api/composite", headers=HEADERS, json={
"source_task_ids": ["task_a", "task_b", "task_c"],
"layout": "1x3",
"labels": ["(a)", "(b)", "(c)"],
"spacing": 20,
"label_position": "bottom",
"label_font_size": 32,
})
print(r.json())
Webhooks
Instead of polling task completion, you can provide a webhook_url and receive an
HTTP POST callback when the task finishes or fails.
Configuration Levels
Webhooks can be configured at two levels:
| Level | How To Configure | Priority |
|---|---|---|
| API key default | Set webhook_url and optionally webhook_secret when creating or updating the API key |
Used when the request body does not specify a webhook URL |
| Per request override | Send webhook_url in the /api/generate, /api/optimize-prompt, /api/plot, or /api/composite request body |
Overrides the API key default for that task |
Webhook Fields On Generate / Optimize-Prompt / Plot / Composite Requests
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url | string | No | Callback URL that receives the final task result |
webhook_include_image | boolean | No | Include image_base64 in the callback when set to true |
Webhook Payload
POST https://your-server.com/paperbanana
Content-Type: application/json
X-PaperBanana-Event: task.completed
X-PaperBanana-Signature: sha256=a1b2c3d4...
User-Agent: PaperBanana-Webhook/0.1.2
{
"event": "task.completed",
"task_id": "a1b2c3d4e5f6...",
"status": "completed",
"diagram_type": "methodology",
"description": "The diagram shows...",
"image_url": "https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image",
"image_base64": null,
"error": null,
"created_at": "2026-02-15T10:30:00+00:00",
"completed_at": "2026-02-15T10:31:25+00:00"
}
image_base64 is populated only when webhook_include_image is true.
Otherwise, use image_url and the original Bearer token to fetch the PNG. For text-only prompt optimization tasks, image_url is null.
Signature Verification
If the API key has a webhook_secret, callbacks include
X-PaperBanana-Signature with an HMAC-SHA256 signature of the raw request body.
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verifySignature(body, signature, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Retry Policy
If delivery fails because of a network issue or an HTTP status of 400 or above, PaperBanana retries up to three times:
| Attempt | Delay Before Retry |
|---|---|
| Retry 1 | 5 seconds |
| Retry 2 | 15 seconds |
| Retry 3 | 60 seconds |
You can inspect webhook delivery status in the history view of the admin interface.
Example Request With Webhook
curl -X POST https://api.paperbanana.me/api/generate \
-H "Authorization: Bearer pb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"source_context": "Our model uses a 6-layer Transformer encoder...",
"caption": "Figure 1: Transformer architecture",
"iterations": 3,
"webhook_url": "https://your-server.com/pb-callback",
"webhook_include_image": false
}'
import requests
r = requests.post("https://api.paperbanana.me/api/generate", headers=HEADERS, json={
"source_context": "Our model uses a 6-layer Transformer encoder...",
"caption": "Figure 1: Transformer architecture",
"iterations": 3,
"webhook_url": "https://your-server.com/pb-callback",
"webhook_include_image": False,
})
print(r.json())
const res = await fetch("https://api.paperbanana.me/api/generate", {
method: "POST",
headers: {
"Authorization": "Bearer pb_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
source_context: "Our model uses a 6-layer Transformer encoder...",
caption: "Figure 1: Transformer architecture",
iterations: 3,
webhook_url: "https://your-server.com/pb-callback",
webhook_include_image: false
})
});
console.log(await res.json());
Get Task Status And Result
GET/api/tasks/{task_id}
Fetch the current state of a task. Completed tasks always include the final description; image fields are populated only for image-producing task types.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
task_id | string | Task ID returned by generate, optimize-prompt, plot, or composite |
Response 200
{
"task_id": "a1b2c3d4e5f6...",
"status": "completed",
"diagram_type": "statistical_plot",
"image_base64": "iVBORw0KGgo...",
"artifacts": {
"png": {
"format": "png",
"media_type": "image/png",
"download_url": "https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image"
},
"svg": {
"format": "svg",
"media_type": "image/svg+xml",
"download_url": "https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image?format=svg"
},
"pdf": {
"format": "pdf",
"media_type": "application/pdf",
"download_url": "https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image?format=pdf"
}
},
"description": "The diagram shows...",
"error": null
}
diagram_type = "prompt_optimization", the response still includes the optimized description, but image_base64, artifacts, and /image downloads are not available.
Status Lifecycle
| Status | Meaning | Next Step |
|---|---|---|
pending | The task is queued | Continue polling |
running | The pipeline is generating the image | Continue polling |
completed | The image is ready | Use image_base64 for PNG preview or inspect artifacts for downloadable outputs |
failed | The task failed | Inspect error |
curl https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6 \
-H "Authorization: Bearer pb_your_api_key_here"
const res = await fetch("https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6", {
headers: { "Authorization": "Bearer pb_your_api_key_here" }
});
const task = await res.json();
console.log(task);
r = requests.get(
"https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6",
headers=HEADERS
)
task = r.json()
print(task)
Download Task Image
GET/api/tasks/{task_id}/image
Download a generated task artifact. The default response is PNG.
Statistical plots can additionally return SVG or PDF via ?format=svg or ?format=pdf.
Prompt optimization tasks do not produce downloadable image artifacts.
Response 200
Binary file with Content-Type matching the requested format.
Response 404
The task does not exist or the image is not available yet.
curl -o diagram.png https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image \
-H "Authorization: Bearer pb_your_api_key_here"
curl -o plot.svg "https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image?format=svg" \
-H "Authorization: Bearer pb_your_api_key_here"
const res = await fetch("https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image", {
headers: { "Authorization": "Bearer pb_your_api_key_here" }
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
r = requests.get(
"https://api.paperbanana.me/api/tasks/a1b2c3d4e5f6/image",
headers=HEADERS
)
r.raise_for_status()
with open("diagram.png", "wb") as f:
f.write(r.content)
Create API Key
POST/api/admin/keys
Create a new API key with a name and an optional quota. Admin authentication is required.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable label such as research-team or my-app |
quota | integer | No | Initial quota. Omit it for unlimited usage |
webhook_url | string | No | Default webhook URL for tasks submitted with this key |
webhook_secret | string | No | Secret used to sign webhook payloads with HMAC-SHA256 |
Response 201 Created
{
"key": "pb_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"name": "my-app",
"quota_remaining": 100,
"quota_total": 100,
"total_used": 0,
"active": true,
"created_at": "2026-02-15T10:30:00+00:00",
"webhook_url": "https://your-server.com/callback"
}
curl -X POST https://api.paperbanana.me/api/admin/keys \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "quota": 100}'
const res = await fetch("https://api.paperbanana.me/api/admin/keys", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ADMIN_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "my-app", quota: 100 })
});
console.log(await res.json());
ADMIN_HEADERS = {"Authorization": "Bearer YOUR_ADMIN_KEY"}
r = requests.post("https://api.paperbanana.me/api/admin/keys",
headers=ADMIN_HEADERS,
json={"name": "my-app", "quota": 100}
)
print(r.json())
List API Keys
GET/api/admin/keys
Return all API keys with quota and usage metadata. Admin authentication is required.
Response 200
[
{
"key": "pb_a1b2c3...",
"name": "my-app",
"quota_remaining": 87,
"quota_total": 100,
"total_used": 13,
"active": true,
"created_at": "2026-02-15T10:30:00+00:00"
}
]
curl https://api.paperbanana.me/api/admin/keys \
-H "Authorization: Bearer YOUR_ADMIN_KEY"
const res = await fetch("https://api.paperbanana.me/api/admin/keys", {
headers: { "Authorization": "Bearer YOUR_ADMIN_KEY" }
});
console.log(await res.json());
r = requests.get("https://api.paperbanana.me/api/admin/keys", headers=ADMIN_HEADERS)
print(r.json())
Get API Key Detail
GET/api/admin/keys/{key}
Return the full API key record, including recent usage logs. Admin authentication is required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
key | string | The full API key value |
Response 200
{
"key": "pb_a1b2c3...",
"name": "my-app",
"quota_remaining": 87,
"quota_total": 100,
"total_used": 13,
"active": true,
"created_at": "2026-02-15T10:30:00+00:00",
"usage_log": [
{
"id": 1,
"task_id": "abc123...",
"endpoint": "generate",
"created_at": "2026-02-15T11:00:00+00:00"
}
]
}
curl https://api.paperbanana.me/api/admin/keys/pb_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
-H "Authorization: Bearer YOUR_ADMIN_KEY"
api_key = "pb_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
r = requests.get(f"https://api.paperbanana.me/api/admin/keys/{api_key}", headers=ADMIN_HEADERS)
print(r.json())
Update API Key
PATCH/api/admin/keys/{key}
Update quota values, activation state, or webhook defaults. All request fields are optional.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
quota_remaining | integer | No | Set the remaining quota, minimum 0 |
quota_total | integer | No | Set the total quota, minimum 0 |
active | boolean | No | Enable or disable the key |
webhook_url | string | No | Update the default webhook URL. Send an empty string to remove it |
webhook_secret | string | No | Update the webhook signing secret. Send an empty string to remove it |
Response 200
The API returns the updated API key object.
curl -X PATCH https://api.paperbanana.me/api/admin/keys/pb_a1b2c3... \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"quota_remaining": 150, "quota_total": 200, "active": true}'
r = requests.patch(
"https://api.paperbanana.me/api/admin/keys/pb_a1b2c3...",
headers=ADMIN_HEADERS,
json={"quota_remaining": 150, "quota_total": 200, "active": True}
)
print(r.json())
Delete API Key
DELETE/api/admin/keys/{key}
Permanently delete an API key and its usage log. This action cannot be undone.
Response 204 No Content
The response body is empty on success.
curl -X DELETE https://api.paperbanana.me/api/admin/keys/pb_a1b2c3... \
-H "Authorization: Bearer YOUR_ADMIN_KEY"
r = requests.delete(
"https://api.paperbanana.me/api/admin/keys/pb_a1b2c3...",
headers=ADMIN_HEADERS
)
assert r.status_code == 204