PaperBanana API Docs
Generate publication-ready methodology diagrams and statistical plots through a task-based async API.
English
Overview
PaperBanana is an AI-powered API for generating publication-ready academic visuals. It currently supports two workflows:
- Methodology diagrams for architectures, pipelines, and research workflows
- Statistical plots for bar charts, line plots, histograms, scatter plots, and similar figures
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/plot, /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 or plot 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/plot) -> { task_id, status: "pending" }
2. GET /api/tasks/{task_id} -> { status: "running" }
3. GET /api/tasks/{task_id} -> { status: "completed", image_base64: "...", description: "..." }
Optional:
GET /api/tasks/{task_id}/image -> download the PNG directly
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 |
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"
}
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
}'
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
})
});
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,
})
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 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 |
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"
}
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
}'
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
})
});
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,
})
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 or /api/plot request body |
Overrides the API key default for that task |
Webhook Fields On Generate / Plot 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.
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 include a base64-encoded image and a generated description.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
task_id | string | Task ID returned by generate or plot |
Response 200
{
"task_id": "a1b2c3d4e5f6...",
"status": "completed",
"diagram_type": "methodology",
"image_base64": "iVBORw0KGgo...",
"description": "The diagram shows...",
"error": null
}
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 or the image download endpoint |
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 the final PNG directly. This endpoint is available only after the task reaches
completed.
Response 200
Binary PNG image with Content-Type: image/png.
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"
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