REST API

PaperBanana API Docs

Generate methodology diagrams, optimize illustration prompts, create statistical plots, and compose figures through a task-based async API.

Languages
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 TypeScopeHow 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
Quota model Each API key has its own quota. Every 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
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
aspect_ratiostringNoOptional 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_formatsarray[string]NoOptional methodology diagram output formats. Supported value: png. PNG preview remains available for polling responses
vlm_providerstringNoOptional VLM provider override. Supported values: gemini, openrouter, kie, apimart. If omitted, the server keeps its configured default provider
vlm_modelstringNoOptional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model
image_modelstringNoOptional 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_resolutionstringNoOptional 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_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"
}
VLM provider examples To route requests through KIE:
{
  "vlm_provider": "kie",
  "vlm_model": "gemini-2.5-flash"
}
To route requests through APIMart:
{
  "vlm_provider": "apimart",
  "vlm_model": "gemini-2.5-flash"
}
Kie image model example To use GPT Image 2 through Kie:
{
  "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.
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,
    "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

FieldTypeRequiredDescription
pdf_base64stringYesBase64-encoded PDF content. A data:application/pdf;base64,... data URL is also accepted
captionstringYesFigure title or communicative intent for the generated methodology diagram
pagesstringNoOptional 1-based page selection, for example 1, 1,3-5, or 5-3. If omitted, all pages are used
iterationsintegerNoRefinement iterations from 1 to 10. Default is 3
aspect_ratiostringNoOptional target aspect ratio. Same values as /api/generate
output_formatsarray[string]NoOptional methodology diagram output formats. Supported value: png
vlm_providerstringNoOptional VLM provider override. Supported values: gemini, openrouter, kie, apimart
vlm_modelstringNoOptional VLM model override
image_modelstringNoOptional Kie image model override
image_resolutionstringNoOptional Kie image resolution override
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"
}
PDF handling The request body is JSON, so callers should base64-encode the PDF file. The raw PDF is saved under the configured output directory for traceability, but the task history stores only the file path, selected pages, and extracted text length.
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

FieldTypeRequiredDescription
descriptionstringYesThe prompt or detailed description to optimize
source_contextstringNoOptional paper excerpt or methodology context used to keep the rewrite grounded
captionstringNoOptional figure caption or communicative intent
diagram_typestringNoTarget output type for the optimized prompt. Supported values: methodology, statistical_plot. Default is methodology
guidelinesstringNoOptional custom style guidelines. When omitted, the server uses its built-in methodology or plot guideline set
vlm_providerstringNoOptional VLM provider override. Supported values: gemini, openrouter, kie, apimart
vlm_modelstringNoOptional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model
webhook_urlstringNoOptional callback endpoint to receive the final result
webhook_include_imagebooleanNoAccepted for API consistency, but ignored because this task does not generate images

Response 202 Accepted

{
  "task_id": "b7c8d9e0f1a2...",
  "status": "pending"
}
Result shape When the task completes, the optimized prompt is returned in the task response field 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

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
aspect_ratiostringNoOptional 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_formatsarray[string]NoOptional output formats for plots. Supported values: png, svg, pdf. PNG preview remains available for polling responses
vlm_providerstringNoOptional VLM provider override. Supported values: gemini, openrouter, kie, apimart. If omitted, the server keeps its configured default provider
vlm_modelstringNoOptional VLM model override. If omitted, the current model is preserved or the selected provider falls back to its default model
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"
}
VLM provider examples To route requests through KIE:
{
  "vlm_provider": "kie",
  "vlm_model": "gemini-2.5-flash"
}
To route requests through APIMart:
{
  "vlm_provider": "apimart",
  "vlm_model": "gemini-2.5-flash"
}
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,
    "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

FieldTypeRequiredDescription
source_task_idsarray[string]YesAt least two completed task IDs owned by the same API key
layoutstringNoGrid layout as RxC such as 1x3 or 2x2, or auto. Default is auto
labelsarray[string]NoOptional explicit panel labels. Omit to auto-generate (a), (b), (c), ...
spacingintegerNoSpacing in pixels between panels and around the outer margin. Default is 20
label_positionstringNoWhere labels are rendered relative to each panel. Supported values: top, bottom. Default is bottom
label_font_sizeintegerNoFont size for panel labels. Default is 32
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": "c0ffee123456...",
  "status": "pending"
}
Source task requirements Every ID in 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:

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, /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

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. 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:

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 always include the final description; image fields are populated only for image-producing task types.

Path Parameters

ParameterTypeDescription
task_idstringTask 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
}
Text-only tasks For diagram_type = "prompt_optimization", the response still includes the optimized description, but image_base64, artifacts, and /image downloads are not available.

Status Lifecycle

StatusMeaningNext Step
pendingThe task is queuedContinue polling
runningThe pipeline is generating the imageContinue polling
completedThe image is readyUse image_base64 for PNG preview or inspect artifacts for downloadable outputs
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 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

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