Developer Docs

Pixfaro API & MCP

Every image model behind one endpoint: your agent asks for an image; Pixfaro holds the provider accounts, keys, retries and format differences, and bills one prepaid balance.

REST API REST

Base URL https://api.pixfaro.com. JSON everywhere. Errors use one envelope:

{ "error": { "code": "insufficient_balance", "message": "…", "request_id": "rq_a1c4f0" } }

Every response carries an x-request-id header — include it when you contact support. Money is always a USD decimal string ("0.080", never a float). Times are ISO 8601 UTC.

Authentication

Principal How
API key Authorization: Bearer pf_live_…
MCP OAuth 2.1 at https://mcp.pixfaro.com (remote) or PIXFARO_KEY env (stdio)

Keys are created on the dashboard. Scopes: generate (default — image endpoints only) and full (adds balance, billing, logos, and brand-kit management).

New accounts must verify their email before generating — unverified calls return 403 email_unverified. The $1 welcome credit lands on verification (instantly for Google/GitHub signups).

POST /v1/images/generations

Generate an image. Sync — the response is the finished image.

{
  "model": "nano-banana-2",
  "prompt": "a lighthouse at night, minimal flat style",
  "aspect_ratio": "16:9",
  "resolution": "1K",
  "overlay": "default"
}
Field Required Notes
model yes id from GET /v1/models
prompt yes 1–4000 chars
aspect_ratio no "w:h", e.g. "1:1" (default), "16:9", "9:16"
resolution no per-model tiers from GET /v1/models prices (default 1K)
overlay no corner branding: "default" applies your saved brand kit, or an explicit object (below)

n > 1 and response_format other than "url" are not supported yet — one image per request, delivered as a hosted URL (your agent's context never carries base64).

200:

{
  "id": "img_8f2a…", "url": "https://api.pixfaro.com/i/…", "model": "nano-banana-2",
  "resolution": "1K", "latency_ms": 10400, "cost": "0.080", "balance_after": "12.32",
  "overlay_applied": false, "request_id": "rq_a1c4f0"
}

Errors: 402 insufficient_balance (body includes balance, needed, topup_url), 400 invalid_model / invalid_prompt / invalid_request, 403 email_unverified, 409 model_disabled, 429 rate_limited, 502 provider_failedno charge, and the body says so explicitly: "charged": false.

Overlay object

Brands a corner of the image with your handle or logo. Exactly one of text (≤ 64 chars) or logo_id (a logo_… from POST /v1/logos):

{ "overlay": { "text": "@yourbrand", "position": "bottom-right", "opacity": 0.9 } }

Optional: position (default bottom-right), opacity (0.2–1.0; text defaults to 0.9, logos to 1.0), logo_style (sticker — default — | shadow | outline | none), size, margin, and for text font / weight / color ("auto" picks ink or paper per corner brightness). Save your defaults once via PUT /v1/brand-kit, then "overlay": "default" everywhere. A bad overlay fails the request before generation — you are not charged.

POST /v1/images/edits

Edit a previous generation with a natural-language instruction. Same response shape as generations.

{ "model": "nano-banana-2", "image": "img_8f2a…", "instruction": "make the sky darker" }

GET /v1/models

Public, no auth. Cached ~5 min.

[{ "id": "nano-banana-2", "name": "Nano Banana 2",
   "best_for": "general-purpose images: blog art, social posts, mockups",
   "p50_ms": 10700, "p95_ms": 13600, "price": "0.080",
   "prices": { "1K": "0.080", "2K": "0.121", "4K": "0.181" },
   "mode": "sync", "enabled": true }]

price is the 1K figure; prices maps every supported resolution tier to its retail price. enabled: false marks models that are coming soon (calling one returns 409 model_disabled).

POST /v1/renders

Typeset a card from a template instead of generating one — flat $0.02, ~2s, sharp type. Full reference: Card templates.

curl https://api.pixfaro.com/v1/renders -H "Authorization: Bearer pf_live_…" \
  -H "Content-Type: application/json" \
  -d '{"template":"quote-card","slots":{"quote":"Ship it.","handle":"@you"},"style":"auto"}'

A render is a generation: it returns an img_…, lands in your history, and is debited and refunded by the same rules.

GET /v1/templates

Public, no auth — the card catalog with slots, style axes and price. See Card templates.

GET /v1/balance

Requires scope full (a default generate key gets 403 insufficient_scope).

{ "balance": "12.32" }

Account endpoints (scope full)

Endpoint What
POST /v1/topups { "amount_usd": 25 }{ "checkout_url": … } (Stripe; integer 5–1000, default 10)
GET/PATCH /v1/autoreload auto top-up: { enabled, threshold, amount, monthly_cap } — explicit opt-in
POST /v1/logos upload a transparent PNG (≤ 1 MB, ≤ 2048px/side, ≤ 10 live logos) — raw binary body, or JSON { "image": "<base64 or data URI>", "name"? }
GET /v1/logos · DELETE /v1/logos/:id list / remove logos
GET/PUT/DELETE /v1/brand-kit saved overlay defaults used by "overlay": "default"
POST /v1/assets upload a PNG/JPEG (≤ 5 MB, ≤ 4096px/side, ≤ 100 live) for card slots — raw binary with ?kind=, or JSON { "kind", "data" }
GET /v1/assets · DELETE /v1/assets/:id list / remove assets
GET/PUT/DELETE /v1/brand-kit/identity name, handle, avatar and palette — what "default" resolves to in a card slot

Top-ups, keys, and usage are also on the dashboard.

POST /v1/abuse-reports

Public, no auth — report a generated image that violates our acceptable use policy: { "image_url", "reason", "details"?, "reporter_email"? }image_url must be a Pixfaro-served image link (…/i/…); reason is one of csam, nonconsensual, violence_hate, infringement, other.

Rate limits

Fair-use limits apply on auth and abuse-prone endpoints (429 rate_limited). There is no fixed per-key request cap today; sustained high volume is welcome — talk to us if you're planning a big batch.

Versioning

Path-versioned (/v1/). Additive changes (new optional fields) don't bump the version; breaking changes go to /v2/ with 6 months of /v1/ support. The x-pixfaro-deprecation header announces sunsets ahead of time.

Card templates CARDS

Not every image needs a model. A card is typeset, not generated: you send text, we render it in a real browser and hand back a hosted PNG. Flat $0.02, about two seconds, and the type comes out sharp — which is the one thing image models still get wrong.

Same key, same balance, same MCP as everything else. A render returns an img_… at /i/…, shows up in your history, and can be downloaded, edited or overlaid like any other image.

curl https://api.pixfaro.com/v1/renders \
  -H "Authorization: Bearer pf_live_…" -H "Content-Type: application/json" \
  -d '{"template":"quote-card",
       "slots":{"quote":"Stop mistaking silence for nobody'\''s home.","handle":"@dashaworks"},
       "style":"auto"}'
{ "id": "img_4e1807777fc895e5642a",
  "url": "https://api.pixfaro.com/i/usr_…/img_….png",
  "kind": "render", "template": "quote-card", "size": "16:9",
  "style": { "palette": "charcoal", "font": "schibsted", "layout": "bold-left", "shadow": true },
  "cost": "0.020", "balance_after": "12.30", "latency_ms": 1412 }

GET /v1/templates

Public, no auth. Every template with its slots, style axes and exact price — enough to build a form from, which is what the dashboard does.

{ "templates": [
  { "id": "quote-card", "name": "Quote card", "tier": "simple", "price": "0.020",
    "best_for": "one line worth quoting — hooks, testimonials, pull quotes",
    "sizes": ["16:9", "1:1", "4:5", "og"],
    "slots": [ { "name": "quote", "type": "text", "required": true, "max": 280, "hint": "the line itself" } ],
    "styles": { "palettes": ["paper", "charcoal", …], "fonts": ["schibsted", …], "layouts": ["bold-left", …], "shadow": true } } ] }

POST /v1/renders

Field What
template id from GET /v1/templates
slots the template's slot values (below)
size 16:9 1200×675 · 1:1 1080×1080 · 4:5 1080×1350 · og 1200×630 — default 16:9
style omit for the template default, "auto", "brand", or an object of axes
scale device pixel ratio: 1, 2 (default) or 3 (+$0.01)
overlay optional corner branding, exactly as on a generation

Scope generate is enough — a render is a generation.

Slots

Text slots take a string and are rejected (never truncated) past their max, so you never pay for a card that quietly lost its last sentence. Image slots take only Pixfaro-hosted references:

Value Meaning
"ast_…" or { "asset": "ast_…" } an asset you uploaded (POST /v1/assets)
"https://api.pixfaro.com/i/…" one of your own generated images
"data:image/png;base64,…" inline PNG or JPEG, ≤ 2 MB — validated as an image, not as a string
"default" from your brand identity (PUT /v1/brand-kit/identity)

Arbitrary URLs are refused: the renderer is a real browser, and a fetchable slot would make every card a probe into whatever that browser can reach. "default" also works on the name and handle text slots.

Style

"auto" picks a random combination and avoids the one you used last on this template — two cards in a row that differ only in wording read as a bug, not as a series. "brand" paints your saved colours (identity.palette). An object pins whichever axes you name and defaults the rest:

{ "style": { "palette": "charcoal", "font": "instrument", "layout": "block", "shadow": true } }

The chosen combination comes back in the response, so a card you like can be re-rendered exactly.

Errors

Code Meaning
invalid_template unknown id — see GET /v1/templates
invalid_slot with a slot field naming which one
asset_not_found unknown, deleted, or not yours (404, never an existence oracle)
render_busy 429, renderer at capacity — retry; not charged
render_failed 502; refunded, and "charged": false says so

POST /v1/assets

Scope full. PNG or JPEG, ≤ 5 MB, ≤ 4096 px/side, 100 live per account. Raw binary with ?kind=avatar, or JSON:

curl https://api.pixfaro.com/v1/assets \
  -H "Authorization: Bearer pf_live_…" -H "Content-Type: application/json" \
  -d '{"kind":"avatar","data":"data:image/png;base64,…"}'

{ "id": "ast_…", "kind": "avatar", "width": 512, "height": 512 }

GET /v1/assets?kind=avatar lists yours; DELETE /v1/assets/:id removes one. Assets are private — they are never served at a public URL; the renderer reads them server-side and inlines them into the card.

Uploading a photo of a person means you have the right to use it. Personas are self-likeness only, and attributing words to someone who did not say them is impersonation whatever the styling — see the acceptable use policy.

Brand identity

PUT /v1/brand-kit/identity (scope full) is what "default" resolves to:

{ "name": "Dasha", "handle": "@dashaworks", "avatar_asset_id": "ast_…",
  "palette": { "bg": "#101014", "ink": "#fdfdfb", "accent": "#b4e33d" } }

It sits beside the overlay brand kit (/v1/brand-kit) and is cleared separately.

Template catalog

id Tier Slots
quote-card simple · $0.02 quote, name?, handle?, avatar?
post-card simple · $0.02 text, name, handle?, platform?, date?, avatar?

More arrive with composition — an image slot you can fill with a generation in the same request.

Fonts

Cards are set in self-hosted SIL OFL 1.1 faces — no font CDN in the render path, so a card is deterministic and its typography does not depend on a third party being up. The licence text ships beside every family we serve.

Face Class Cyrillic Licence
Schibsted Grotesk sans OFL-1.1
Space Grotesk sans OFL-1.1
Manrope sans OFL-1.1
Inter sans OFL-1.1
Syne sans OFL-1.1
Bricolage Grotesque sans OFL-1.1
Fraunces serif OFL-1.1
Playfair Display serif OFL-1.1
DM Serif Display serif OFL-1.1
Instrument Serif serif OFL-1.1
IBM Plex Mono mono OFL-1.1

Cyrillic text set in a Latin-only face is switched to the closest Cyrillic-capable face of the same class (Manrope, Playfair Display or IBM Plex Mono), rather than falling through to a system font mid-headline.

MCP server MCP

One MCP connection gives your agent every major image model. Two ways in:

Transport For How
Remote (streamable HTTP + OAuth 2.1) claude.ai, Claude Desktop, any HTTP-capable client add https://mcp.pixfaro.com/mcp — the client runs the OAuth flow, you sign in with your Pixfaro account
Local (stdio, npm) Claude Code, Cursor, Windsurf, config-file setups npx -y @pixfaro/mcp with PIXFARO_KEY in env

Source: github.com/pixfaro/mcp — the client is intentionally small enough to read before you hand it a key.

Setup — no terminal needed

The remote server is the easiest path on every desktop app: paste one URL, sign in with your Pixfaro account in the browser window that opens, done. No API key ends up in any config file.

Claude (claude.ai, Claude Desktop — any plan):

  1. Settings → Connectors → Add custom connector
  2. URL: https://mcp.pixfaro.com/mcpAdd → sign in when prompted. On Team/Enterprise an org Owner adds it once for everyone (Organization settings → Connectors → Custom → Web).

Cursor — one click:

Add to Cursor →

or by hand: Cursor Settings → MCP → Add new MCP server, type URL, address https://mcp.pixfaro.com/mcp — Cursor runs the OAuth flow itself.

ChatGPT (Pro / Plus / Business / Edu):

  1. Settings → toggle Developer mode on (since the July 2026 rename the connector list lives under Plugins; before that it was Connectors → Advanced)
  2. Add a custom connector: name pixfaro, URL https://mcp.pixfaro.com/mcp, transport streamable HTTP, authentication OAuth → sign in.

Setup — command line & config files

Claude Code:

claude mcp add pixfaro -e PIXFARO_KEY=pf_live_… -- npx -y @pixfaro/mcp

Windsurf or any stdio client (config file; key from the dashboard):

{
  "mcpServers": {
    "pixfaro": {
      "command": "npx",
      "args": ["-y", "@pixfaro/mcp"],
      "env": { "PIXFARO_KEY": "pf_live_…" }
    }
  }
}

Clients that speak streamable HTTP can also use the remote form in the same file — no key, OAuth on first use:

{ "mcpServers": { "pixfaro": { "url": "https://mcp.pixfaro.com/mcp" } } }

Tools

Tool What it does
generate_image prompt (+ optional model, aspect_ratio, resolution, overlay — incl. "default" for your saved brand kit) → hosted image URL, with cost and remaining balance in the reply
edit_image natural-language edit of a previous generation by its img_… id (keeps the source's shape unless you pass aspect_ratio)
render_template typeset card from a template — slots in, hosted PNG out (flat $0.02, ~2s); sharper type than any model, and cheaper
upload_asset store a PNG/JPEG (≤ 5 MB) and get an ast_… id to pass into a card slot
list_models live models with price, latency, and what each is best for
list_templates card templates with their slots, style axes and exact price
get_balance current prepaid balance — needs scope full (stdio: a full-scope key; remote OAuth grants generate by default)

Replies are agent-readable text carrying a hosted URL — never base64, so your agent's context stays small. When the balance runs low, replies append a top-up warning.

Environment (stdio)

Variable Meaning
PIXFARO_KEY API key (pf_live_…) from pixfaro.com
PIXFARO_API_URL endpoint override (staging / self-hosted)

CLI

The same repo ships the pixfaro CLI for scripts, CI, and pipelines — the literal one-command time-to-first-image:

export PIXFARO_KEY=pf_live_…
npx pixfaro gen "a lighthouse at night, minimal flat style" -a 16:9 -o cover.png
npx pixfaro edit img_8f2a… "make the sky darker" -o v2.png
npx pixfaro models
npx pixfaro balance

-o downloads the image; without it the CLI prints the hosted URL. A failed download never swallows the URL you already paid for.