REST API

PaperBanana API Docs

Generate publication-ready methodology diagrams and statistical plots through a task-based async API.

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 TypeScopeHow 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
Quota model Each API key has its own quota. Every 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
Polling cadence Poll every 3 to 5 seconds. If you do not want polling, configure a webhook and wait for the callback instead.

Error Handling

StatusMeaningTypical Cause
401UnauthorizedMissing or invalid API key / admin key
403ForbiddenAPI key disabled or quota exhausted
404Not foundUnknown task ID, missing key, or inaccessible image
422Validation errorMissing required fields or invalid JSON in data_json
500Server errorInternal 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

FieldTypeRequiredDescription
source_contextstringYesMethodology text or a relevant excerpt from the paper
captionstringYesFigure title, for example Figure 1: Transformer encoder architecture
iterationsintegerNoRefinement iterations from 1 to 10. Default is 3
webhook_urlstringNoOptional callback endpoint to receive the final result
webhook_include_imagebooleanNoWhen true, include image_base64 in the webhook payload

Response 202 Accepted

{
  "task_id": "a1b2c3d4e5f6...",
  "status": "pending"
}
Quota consumption One quota unit is deducted when the task is submitted, not when it finishes.
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

FieldTypeRequiredDescription
data_jsonstringYesA JSON string containing the plotting data
intentstringYesDescription of the desired chart, axes, and styling expectations
iterationsintegerNoRefinement iterations from 1 to 10. Default is 3
webhook_urlstringNoOptional callback endpoint to receive the final result
webhook_include_imagebooleanNoWhen true, include image_base64 in the webhook payload

Response 202 Accepted

{
  "task_id": "f7e8d9c0b1a2...",
  "status": "pending"
}
About 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:

LevelHow To ConfigurePriority
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

FieldTypeRequiredDescription
webhook_urlstringNoCallback URL that receives the final task result
webhook_include_imagebooleanNoInclude 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 delivery 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:

AttemptDelay Before Retry
Retry 15 seconds
Retry 215 seconds
Retry 360 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

ParameterTypeDescription
task_idstringTask 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

StatusMeaningNext Step
pendingThe task is queuedContinue polling
runningThe pipeline is generating the imageContinue polling
completedThe image is readyUse image_base64 or the image download endpoint
failedThe task failedInspect 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

FieldTypeRequiredDescription
namestringYesHuman-readable label such as research-team or my-app
quotaintegerNoInitial quota. Omit it for unlimited usage
webhook_urlstringNoDefault webhook URL for tasks submitted with this key
webhook_secretstringNoSecret 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"
}
One-time visibility Store the full API key when you create it. That full value is only returned in the creation response.
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

ParameterTypeDescription
keystringThe 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

FieldTypeRequiredDescription
quota_remainingintegerNoSet the remaining quota, minimum 0
quota_totalintegerNoSet the total quota, minimum 0
activebooleanNoEnable or disable the key
webhook_urlstringNoUpdate the default webhook URL. Send an empty string to remove it
webhook_secretstringNoUpdate 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