What's new

Updates

Notable changes to the makesPDF API, catalog, and product. Newest first.

apienhancex402

/enhance is now wallet-payable (x402)

Pay for a PDF/UA-1 + PDF/A-2A remediation job with a USDC wallet — no API key, no account. The 202 envelope hands you an opaque pollToken to poll the job and download the artifact when it's ready.

POST /api/v1/enhance now accepts x402 settlement as authentication. Send the PAYMENT-SIGNATURE header, settle USDC on Base (Sepolia in test, mainnet in prod), and the producer returns a 202 carrying a freshly-minted pollToken:

{
  "jobId": "...",
  "status": "queued",
  "statusUrl": "/api/v1/enhance/jobs/...",
  "estimatedSeconds": 240,
  "pollToken": "Rh3...n_w"
}

Re-present that token as Authorization: Bearer <pollToken> on the follow-up calls — GET /api/v1/enhance/jobs/:id, POST .../cancel, and GET /api/v1/artifacts/:id/{pdf|source}. The server hashes the presented token and matches against the job row's stored pollTokenHash. Mismatch returns the same 404 shape as an authed owner-mismatch, so jobIds can't be probed.

The token is bearer-equivalent for that one job's lifetime — agentic callers persist it, humans wouldn't drive this path directly. Replaying the same PAYMENT-SIGNATURE within the 5-minute idempotency cache window returns the cached 202 verbatim, including the original pollToken; this is the supported retry path for connection drops between settlement and 202.

Why this matters: x402 itself is stateless request/response, but the spec doesn't preclude polling — a 202 with { jobId, pollToken } is fully conformant. /enhance takes minutes (vision per page, multiple LLM calls, in-place tagging), well past the 30s Workers CPU budget, so synchronous fulfilment was never on the table. Wallet-authenticated polling closes the loop and unblocks the route's listing in the CDP Bazaar discovery index.

Listing is the one operation wallet callers can't do — GET /api/v1/enhance/jobs stays user-scoped. You know your jobIds because we returned them.

One caveat for SDK authors. Idempotency-Key replays (same key, same wallet, second submission) return the existing jobId but with pollToken: null — the original raw token isn't recoverable from its stored hash. Use the signature replay path (the same PAYMENT-SIGNATURE within the 5-minute cache window) when you need the original pollToken back; use Idempotency-Key only when you've held onto the first pollToken yourself.

apimarkdownvscode

Markdown → PDF without an account

POST /api/v1/md now accepts unauthenticated requests for short documents — render Markdown to PDF without signing up. Built for the VS Code plugin funnel, open to any caller.

You can now call POST /api/v1/md without an API key. Send Markdown, get a styled PDF back — no signup, no Bearer token, nothing to configure. It's the same deterministic pipeline as the authed call (PDF/A-2A + PDF/UA-1, embedded fonts, tagged structure tree), routed through stricter per-IP limits.

The numbers, per IP address:

  • 60 requests per hour
  • 200 requests per day
  • 20 pages per render (input is also capped at 200 KB of Markdown, same as the authed path)

Hit any of those and the response carries a tip field nudging sign-up. Successful anonymous responses include an X-MakesPDF-Tip header pointing at the higher-limit story; the VS Code plugin surfaces it as a one-time toast.

Every other /api/v1/* endpoint stays authed — /render, /preview, /templates, /enhance, the lot. This carve-out is scoped to /md because the cost ceiling is bounded (no AI call, sub-second pure computation) and the abuse surface is small. Sign up when you want higher limits, saved renders, custom fonts, stored templates, or any of the billed endpoints.

The headline use case is the VS Code plugin — install, open a .md file, run "Convert Markdown to PDF", get a PDF. No settings page detour. Same shape works from curl, a shell script, or any HTTP client:

curl -X POST https://makespdf.com/api/v1/md \
  -H "Content-Type: application/json" \
  -d '{"markdown": "# Hello\n\nSome **bold** text."}' \
  -o output.pdf

A stale or invalid Bearer token still returns 401 — we never silently downgrade an auth attempt to anonymous, so paying callers see a clear "your key is broken" signal instead of a quiet rate-limit cliff.

dslbuilderbarcodesshipping-label

Real Code 39 + Code 128 barcodes via barcode()

New barcode() builder generates scannable Code 39 and Code 128 barcodes as native PDF vector graphics — no font, no rasterisation, dynamic values via {{template}} placeholders.

Templates can now render real, scannable barcodes. The new barcode() DSL builder produces Code 39 or Code 128 symbologies as native PDF vector graphics — no font to embed, no image to fetch, no rasterisation.

barcode("{{trackingNumber}}", {
  format: "code128", // "code128" (default) or "code39"
  width: 240,
  height: 56,
  "show-text": true,
  alt: "Tracking {{trackingNumber}}",
});

The value field accepts {{template}} placeholders and resolves at render time, so the same template renders a different barcode for every payload. Code 128 (default) is the dense general-purpose choice and accepts ASCII 32–127. Code 39 is the older alphanumeric format that's still common in legacy logistics workflows.

Module width is derived from the requested image width and the encoded length so the bars always fill the box minus the standard quiet zones. Pick a width that yields ≥ 0.5pt per module for reliable consumer-scanner reads.

The two shipping label templates in the library (Compact 4×6 and Standard A5) have been migrated to use real barcodes.

Skill: see pdf-template-author.md §Barcodes.

dslbuilderqr-codes

QR codes as native PDF vector graphics

New qr() builder generates scannable QR codes — versions 1–40, all four ECC levels, auto-mode selection, dynamic values via {{template}} placeholders, no font, no rasterisation.

Templates can now render QR codes alongside the Code 39 / Code 128 barcodes we shipped earlier today. The new qr() DSL builder produces QR codes as native PDF vector graphics — no font to embed, no image to fetch, no rasterisation.

qr("https://pay.example.com/inv/{{invoice.number}}", {
  ecc: "M", // "L" | "M" (default) | "Q" | "H"
  size: 120, // total width/height in pt (square)
  alt: "Scan to pay invoice {{invoice.number}}",
});

The value accepts {{template}} placeholders and resolves at render time, so the same template renders a different code for every payload. The encoder auto-selects the smallest QR version that fits (21×21 up to 177×177 modules), picks the most efficient encoding mode (numeric, alphanumeric, or UTF-8 byte), and chooses the lowest-penalty mask per the ISO/IEC 18004 spec.

Pick the error-correction level for the conditions the code will live in: L for clean on-screen URLs, M (default) for everyday print, Q for receipts and dim lighting, H for thermal labels and anywhere damage is likely. Higher ECC means a larger symbol for the same payload — aim for a module width of at least 1pt so phone cameras can resolve it reliably.

The three invoice templates in the library (Classic, Modern, Compact) now include a "scan to pay" QR next to their payment-instructions block. Skill: see pdf-template-author.md §QR Codes.

ai-chattemplates

AI chat now auto-saves your templates — with a real revision history

Every time the assistant changes the DSL, the template is saved (or updated) automatically with a one-sentence description of what changed. Each save is recorded as its own revision, and the new per-template page has a History tab that lists them all in order.

The /ai/chat authoring loop used to be ephemeral: if you closed the tab between turns you lost any in-progress template. That's gone. Every successful DSL iteration now writes to your /templates list automatically — first iteration creates a new row, subsequent iterations update it in place. Your render-data textarea rides along on every save, so the template and the data you've been iterating against stay in sync.

Each save also captures a one-sentence description of what changed in that turn ("Adds a totals row with grid-aligned amount column.", "Switches header from row to column."), and each save is now a proper revision — not just a value that gets blown away on the next turn.

Click into any template (the cards on /templates now open a per-template page at /templates/<id>) and you'll see two tabs in the left column: Chat and History. The History tab lists every save against that template, newest first, with the timestamp and the change description. Linkable too — /templates/<id>?tab=history opens straight to the list.

Re-opening a saved template via Modify Template rehydrates the chat exactly as you left it: same DSL in the workspace, same render data in the panel, same template id under the hood, so the next AI iteration goes straight to update mode. There's no separate "save" or "publish" step — every change is a save, and every save is a revision you can scroll back through.

If you've typed render data between AI turns and try to close the tab, the browser's standard "Leave site?" prompt will still warn you, since data-only edits aren't bundled into a save until the next DSL change ships them through.

apitemplates

Library templates are first-class

Render `templateId: invoice-classic` directly through /api/v1/render — no more fork-then-render. Library and saved templates share one endpoint family.

You can now render any curated library template by ID without first forking it into your account. POST /api/v1/render with {"templateId":"invoice-classic","data":{...}} works for every caller — Bearer-token, session, or x402 wallet — and bills the same way as a saved template.

Behind the scenes, the curated library and your saved templates now live in the same templates table, distinguished only by ownership. That collapses the API surface: /api/v1/library is gone, and /api/v1/templates covers both. List your own with ?owner=mine, browse the library with ?owner=library, or hit the default to see everything you can read. GET /api/v1/templates/:id returns either kind, and PUT / DELETE continue to require ownership (library rows are read-only — they're seed-managed).

The MCP toolset shrinks to match: list_library and get_library_template are removed, and list_templates / get_template cover both surfaces. A new owner filter on list_templates lets agents narrow to library candidates without a separate tool call.

If you were following the documented "fork-then-render" recipe in your client code, you can drop steps 1–3 and call /api/v1/render directly against the library templateId. Forking is still useful when you want to customise the DSL — pull, edit, POST /api/v1/templates, render your copy.

ai-chattemplates

Modify Template now opens in AI chat

The Use Template button on /templates is now Modify Template — it drops you into /ai/chat with the template already loaded, rendered, and ready to customize.

The button on each card in the Template Library and Your Templates is now Modify Template. Clicking it opens /ai/chat with the chosen template already fetched, pre-rendered into the workspace pane, and primed in the system prompt. You see the starting PDF first, then describe the changes you want — colours, sections, copy, layout — and the assistant edits the DSL and re-renders in place.

When you're happy, save the template. Library seeds become a new row under your account; user-template seeds update the existing row in place. The workspace then shows the API recipe — a templateId plus a copy-pasteable two-step POST /api/v1/render snippet — so the path from "I like this" to "wired into my backend" is one screen.

The form-fill detail page at /templates/<id> is gone. The library exists to launch you into API integration, not to compete with consumer "fill in fields, download a PDF" tools — modifying a template through chat is faster and lands you with the artifact you actually need.

apienhance

/enhance now ships regenerate + both modes

/enhance lifts the in-place-only restriction. mode=regenerate rebuilds a clean DSL source you can re-render forever; mode=both ships the tagged original and the regenerated source side-by-side, billed once.

POST /api/v1/enhance no longer rejects non-in-place modes. The async pipeline supports all three modes end-to-end:

  • in-place — tag the original PDF byte-for-byte. Preserves every pixel — same fonts, layout, signatures of authenticity — with the PDF/UA-1 tag tree layered on top.
  • regenerate — rebuild from a clean DSL source we hand back. You get an editable template (visible in /ai/chat or /settings/renders) that you can re-render with new data forever. No more paying the remediation tax every time the document changes.
  • both — run both. The job row now links two artifacts: the in-place tagged PDF (primary) and the regenerated DSL render (secondary). Each is downloadable on /settings/renders and through the GET /api/v1/artifacts/:id/pdf endpoint. Billed once on the union of pages.

The /enhance UI has a re-enabled output-mode dropdown plus a "Switch to regenerate" button on the in-place-impossible 400 response — the same pre-flight that rejected an in-place submission for a font-program issue now hands you a one-click resubmit with mode: "regenerate" and the same upload.

Pricing is unchanged: 10 credits per page on success, the first page is a free preview, partial outcomes are not billed. mode: "both" is the same per-page rate as either single mode — you don't pay twice for two artifacts.

The job status response now carries secondaryArtifactId and secondaryDownloadUrl for mode: "both" runs. Single-mode jobs leave those null. See the API docs for the response shape.

enhancevalidateapi

/enhance refuses upfront when in-place can't help

/enhance now runs your PDF through veraPDF before queueing, and refuses upfront when the failing rules require regenerate. /validate surfaces the same recommendation so you can preview the routing decision before spending credits.

Until today, the only way to learn that an /enhance upload couldn't be fixed in-place was to wait the full pipeline run, get a compliance-gate-failed at the end, and then re-submit with mode: "regenerate". That round-trip is gone.

Every POST /api/v1/enhance now runs a pre-flight veraPDF pass first, cached by sha256(bytes). If the failing rules require a font or content rebuild that the in-place pipeline structurally can't do (CIDSet, embedded font program, ActualText for PUA codepoints), the producer returns 400 error="in-place-impossible" with the full blocker list and a ready-to-resubmit regenerateRequest.options payload — no queue work, no credit hold. Already-compliant uploads short-circuit to 200 status="already-compliant". Unsupportable uploads (encrypted, DRM-protected) get 400 error="unsupportable" with a one-line reason.

The companion POST /api/v1/pdf/validate endpoint surfaces the same routing decision under enhanceRecommendation, so callers (and the /validate page in your browser) can preview which mode is needed before submitting.

In the web UI, the /validate page now renders a per-mode eligibility panel below the rule list — naming the rules that block in-place — and /enhance auto-validates on file select so the recommendation lands before you click submit. If you submit on in-place anyway and the producer rejects it, the error panel surfaces the one-line reason without re-rendering the rule list you already saw.

Regenerate mode itself is still on the Phase 3.5 list — until that lands, the "Switch to regenerate" hand-off is informational. The pre-flight is the immediate win: the same call you make today now tells the truth in sub-second time when the answer is "you'll need a different mode."

apienhance

/api/v1/enhance is now async

Submitting a PDF to /api/v1/enhance now returns 202 with a jobId in under a second. Poll a status URL until the job is ready and we email you when it's done — no more multi-minute HTTP connections that drop on flaky networks.

POST /api/v1/enhance used to hold the HTTP connection open for the entire enhance pipeline. A typical 50-page civic PDF takes 4–5 minutes, which is past the point where consumer browsers and corporate proxies silently drop the socket. We saw it ourselves: the worker kept running, the user saw nothing, the result was unrecoverable.

The endpoint is now async, queue-backed. Submitting a PDF returns 202 in under a second with a jobId and a statusUrl:

{
  "jobId": "f8e1…",
  "status": "queued",
  "statusUrl": "/api/v1/enhance/jobs/f8e1…",
  "estimatedSeconds": 240
}

Poll GET /api/v1/enhance/jobs/:id until status is terminal — start at a 2-second cadence and back off to 8 seconds after the first 30 seconds. The status response carries phase (received / container / in-place / done) and a progress: { current, total } object so you can show a real progress bar instead of a spinner.

When the job succeeds we email you at the address on your account — subject Your enhanced PDF is ready — with a direct download link. The PDF is also surfaced on /settings/renders and via GET /api/v1/artifacts, so you have three paths back to the artifact without keeping any state on the client.

A few details worth pinning down:

  • Idempotency. POST /api/v1/enhance accepts an Idempotency-Key header. Re-submitting the same key returns the existing jobId instead of starting a duplicate run.
  • Cancellation. POST /api/v1/enhance/jobs/:id/cancel flags a job; the consumer notices on its next checkpoint. v1 doesn't actually halt in-flight vision calls, so a job already mid-page will still finish that page before flipping to cancelled.
  • Listing. GET /api/v1/enhance/jobs?status=…&limit=…&offset=… paginates your jobs newest-first, owner-scoped.
  • Billing unchanged. Still 1 credit per 10 pages on success. partial (some pages failed reconstruction) and failed deduct zero.
  • v1 mode restriction. Only mode: "in-place" is accepted on the async path. regenerate and both 400 for now; the artifact-shape policy for jobs that produce two PDFs is the next thing to land.

The /enhance web UI uses the same polling protocol — close the tab, come back tomorrow, you'll find the PDF on /settings/renders and an email in your inbox. No more "did it actually finish?" anxiety on 50-page documents.

apibillingenhance

/api/v1/enhance is now 10 credits / page

/api/v1/enhance now bills 10 credits per page on success (up from 1). Each page drives a Sonnet vision call, and the previous rate didn't cover the cost on plan-credit callers. The /enhance web UI shows the new cost before you submit. x402 wallet callers are unchanged at $0.20 / page in USDC.

POST /api/v1/enhance now bills 10 credits per page on success, up from the previous 1 credit / page. The rate stays flat across all three modes (in-place, regenerate, both) — mode: "both" is still charged once for the in-place page count, with the regenerate render bundled at no marginal charge.

Why the bump. Each page on /enhance drives a per-page Sonnet 4.6 vision call (~$0.05 of OpenRouter spend per page on average), and the old 1-credit rate didn't cover that on the cheaper plans. At Starter's 1,500 monthly credits for $10, a credit costs you ~$0.0067 — so the new 10-credits-per-page rate works out to ~$0.067/page in revenue, enough margin to keep enhance sustainable as a feature rather than a per-call loss leader.

What this means in practice:

  • Free (10 credits/mo) → 1 enhance page / month included.
  • Hobbyist (100 credits/mo) → 10 enhance pages / month.
  • Starter (1,500 credits/mo) → up to 150 enhance pages / month.
  • Growth (2,500 wallet credits / $20 top-up) → 250 enhance pages per $20 of credits, no expiry.

The first page of any upload is still a free preview, partial outcomes still don't deduct, pipeline failures still don't deduct, and the web UI is still unbilled (session-cookie callers run on the house).

x402 wallet callers are unchanged/enhance still settles at $0.20 / page in USDC, independent of the credit rate. The 402 challenge body still parses page count from the upload before quoting, so what's quoted is what's settled.

The /enhance page now shows the new credits-per-page math before you submit ("10 credits per page × N pages = M credits") and the "insufficient credits" prompt links straight to the credit purchase page with the correct shortfall.

apienhanceaccessibility

Opt-in raster fallback on /api/v1/enhance

A new options.acceptRasterFallback flag lets /api/v1/enhance keep going when individual pages can't be reconstructed — failed pages embed as tagged Figure rasters of the original instead of flipping the call to a free partial preview. The output is labelled hybrid and bills at the full per-page rate.

With options.acceptRasterFallback: true, pages the regenerate pipeline can't reconstruct no longer flip the entire upload to a free partial preview — they're embedded as a tagged <Figure> containing a raster of the original page, with /Alt populated from the pdftotext slice for that page.

The default behaviour is unchanged: any page failure still returns partial: true with no credits deducted. The flag is per-request and explicit — there's no workspace-wide "always accept fallback" toggle.

When the flag is set and at least one page falls back, the response shape switches to:

{
  "partial": false,
  "hybrid": true,
  "pdfBase64": "…",
  "report": {
    "pageCount": 5,
    "successfulPages": 3,
    "rasterFallbackPages": [2, 4],
    // …
  },
  "source": {
    "dslJson": "…",
    "markdown": null,
    "markdownAvailable": false,
    "markdownReason": "hybrid output (raster-fallback pages cannot be expressed as markdown)",
  },
}

The mix is disclosed in three places so a downstream consumer can detect it without reading every page: the hybrid: true envelope flag, the report.rasterFallbackPages list, a visible cover page at the front of the PDF listing the fallback page numbers, and a makespdf:hybrid-output = true entry in the document XMP metadata. The X-Enhance-Outcome response header is hybrid instead of full.

Hybrid is billed at the full per-page rate (1 credit / page across the original page count) — the customer opted in and got a usable PDF. Use it for legacy archive bulk uploads where a hybrid-but-honest output beats no deliverable; stay on the default partial path when you'd rather re-submit the broken page than archive a raster of it.

apibillingenhancex402

x402 wallet payments on /api/v1/enhance

Wallet callers can now pay for /api/v1/enhance in USDC. The 402 challenge declares a per-page price ($0.20 / page) computed from the uploaded PDF, so the amount quoted is always the amount settled.

POST /api/v1/enhance now accepts x402 wallet payments alongside Bearer tokens and session cookies. This was the last of the billed /api/v1 endpoints still gated to credit callers; all three (/md, /render, /enhance) now share the same payment surface.

The wallet rate is $0.20 / page in USDC — the same as the credit list price (1 credit / page at $0.20 / credit). No x402 discount, no x402 premium, so the two paths can be repriced independently later.

Because the price is per-page, the 402 challenge body has to know the page count before it's built. The route now parses the uploaded PDF first; the amount declared in the challenge is pageCount × 200000 atomic USDC, and that's exactly what gets settled if the wallet pays.

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "https://makespdf.com/api/v1/enhance",
    "description": "Enhance a non-compliant PDF to PDF/A-2A + PDF/UA-1 ($0.20 / page)",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "amount": "1000000",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo": "0x…"
    }
  ]
}

A few wrinkles worth calling out for wallet callers:

  • Idempotent retry, with one carve-out. Settled wallet renders are cached by signature hash for 5 minutes; a duplicate PAYMENT-SIGNATURE replays the cached JSON envelope verbatim with X-Idempotent-Replay: 1 and never re-runs the pipeline. The exception is mode: "both", which packs two base64 PDFs into one envelope and can blow the 25 MB Workers KV value cap — those responses skip the cache, so a retried mode: "both" request re-pays.
  • Refunds. Validation rejections (malformed PDF, admin-only options, unmatched alt-text overrides) leave the payment in pending for the daily refund cron. Render-stage failures and partial-regenerate outcomes are explicitly marked failed so the cron picks them up faster.
  • options.model stays admin-only. Wallet callers don't have a user row, so they can't be admins; passing options.model returns 403.
validatecomplianceverapdf

Hosted veraPDF validator at /validate

Drop in any PDF and run the ISO reference validator for PDF/A and PDF/UA. Free, no sign-in, nothing kept. Powered by veraPDF 1.28.2 in a Cloudflare Container sidecar.

/validate is now live — a hosted wrapper around veraPDF, the ISO reference validator for PDF/A and PDF/UA. Drop in any PDF, get back a per-profile pass/fail report plus the failed rule list, with clauses and test numbers straight from the spec.

Programmatic callers can hit it directly:

curl -X POST https://makespdf.com/api/v1/pdf/validate \
  -H "Content-Type: application/json" \
  -d "{\"pdfBase64\":\"$(base64 -i sample.pdf)\",\"flavours\":[\"2a\",\"ua1\"]}"

Default profiles are PDF/A-2A and PDF/UA-1, matching what /enhance targets. The full PDF/A-1 / 2 / 3 / 4 family plus PDF/UA-1 is selectable per request.

Privacy. Uploads are held in memory for the length of one request, validated, and discarded as soon as the report renders. Nothing lands in R2, no DB rows are written, file content is not logged.

If a PDF fails compliance, /enhance is the next step — it'll rebuild the document as a tagged, dual-compliant PDF/A-2A + PDF/UA-1 and hand back a clean DSL source you can re-render whenever the document changes.

apibillingenhance

Per-page billing on /api/v1/enhance

/api/v1/enhance now bills 1 credit per page on success. The /enhance web UI shows the cost before you submit and links straight to the credit purchase page when your balance is short.

POST /api/v1/enhance now bills 1 credit per page on success. Previously every call was free for anyone with a session or API key (rate-limited at 50/hour); the per-page rate flips on so the endpoint covers its OpenRouter spend.

The rate is flat across all three modes — in-place, regenerate, and both. mode: "both" is charged once for the in-place page count, with the regenerate render bundled at no marginal charge.

A few invariants worth calling out:

  • Pre-dispatch gate. The page count is read from the uploaded PDF before the enhance pipeline is dispatched. API-key callers without enough credits receive 402 { error: "Insufficient credits", required, available } and no OpenRouter call is made.
  • Partial paths stay free. mode: "regenerate" partials (returned when at least one page fails to reconstruct) deduct zero credits.
  • Failures don't deduct. A pipeline failure leaves your balance untouched; the deduction only happens on the successful response.
  • Web UI is unbilled. The /enhance page in the web app uses your session, which mirrors how /render treats session callers — only API-key traffic consumes credits.

The /enhance page now shows the cost before you submit (e.g. "this will cost 5 credits") and replaces the submit button with a buy-credits link when your balance is short. x402 wallet callers should keep using authed credits on /enhance for now — fixed-price wallet payments are still available on /md and /render.

This is the bottom of the planned $0.20–$0.50/page band. Calibration follows the competitor pricing snapshot.

mcpagentsapirender

render_template lands in MCP — agents can now produce the deliverable

The MCP server now exposes a billed render_template tool, so agents can finish the loop and produce a real PDF without dropping out to curl or the CLI.

When the MCP server first shipped, the billed render endpoint was deliberately left out — the worry was that an agent in a loop could quietly burn through credits with no human in the loop. In practice that decision turned the agent flow into a half-loop: validate, preview (watermarked), save… then leave MCP and run a curl /api/v1/render to get the actual deliverable. That's friction in the one step that matters most.

What's new

render_template is now an MCP tool. It mirrors POST /api/v1/render with the { templateId, data } body shape — render a user-owned saved template (created via save_template or forked from the library) and get back a real PDF as a base64 resource on the response.

The cost is explicit, both in the tool description (which calls out "1 credit per 10 pages" up front) and in the JSON metadata block returned alongside the PDF: creditsDeducted, creditsRemaining, pageCount, and the artifact ID. An agent that wants to be a good citizen can read those and tell the human exactly what the render cost before continuing.

How to use it

The recommended loop is now:

  1. Ask the agent to draft DSL and call validate_dsl until it's clean.
  2. render_dsl_preview to eyeball the layout — free, watermarked, no credits charged.
  3. save_template once you're confident.
  4. render_template with real data to produce the deliverable.

Output behaviour matches /api/v1/render exactly: free-plan callers get the standard plan watermark, paid plans render clean. The artifact lands in your /settings/renders history just like a REST-driven render.

What's still REST-only

The markdown endpoint (/api/v1/md) stays REST-only for now. It's billed too but the input shape is fundamentally different — markdown body, frontmatter options — and it doesn't share the author → save → render loop with the DSL pipeline. Different question, separate decision.

Setup hasn't changed: existing MCP clients (Claude Desktop, Claude Code, Cursor) pick up the new tool the next time they list tools. See /docs/mcp for the full tool table.

marketingcontent

Comparison pages — /compare is live

New /compare section with side-by-side breakdowns of makesPDF against the major PDF generation tools. First live page is react-pdf; nine more competitors are in the pipeline.

The new /compare section is live. It's the home for honest, side-by-side breakdowns of how makesPDF stacks up against the major PDF generation tools — react-pdf, Puppeteer, WeasyPrint, wkhtmltopdf, DocRaptor, PDFShift, typst, iText, and pdfmake.

The first live page is react-pdf vs makesPDF: one-line difference, feature matrix, same-input/two-outputs code example, why people switch, and when react-pdf is still the right call. The remaining nine competitors are listed in the "Coming soon" rail and will land in batches over the next few weeks, starting with the highest-dev-trust pairings (Puppeteer, the "LLM + Puppeteer" wrapper pattern, WeasyPrint).

Each page reuses one shared layout, so adding a new comparison is a markdown-only change. If you want a comparison against a tool we haven't listed, the index page is the place to look — or open an issue and we'll prioritise.

formsaccessibilitydsl

Fillable PDF forms — interactive AND tagged

Templates can ship real fillable AcroForm widgets — text fields, checkboxes, dropdowns — that stay interactive in Reader and Preview and remain tagged for PDF/UA-1 screen readers. Now documented across the docs, skill file, and homepage.

If you author a template with input(), checkbox(), or select(), the rendered PDF carries real AcroForm widgets — the fields stay interactive in Adobe Reader, Apple Preview, and any modern browser PDF viewer. The pipeline shipped a while back; what's new this update is that it's actually documented and discoverable.

Three builders

  • input(opts)/Tx text fields. Single- or multi-line, with maxLength and an accessible tooltip.
  • checkbox(opts)/Btn widgets with pre-baked on/off appearance streams. Pass label to render the box + text together.
  • select(opts)/Ch combo dropdowns. The appearance stream displays the currently selected option's label.

Field name must be unique across the document, and tooltip is the field's accessible name (/TU) — required for PDF/UA-1 conformance. Pair every form template with tagged: true on the doc(...) so the widgets land inside the structure tree.

Why this matters

Most form-capable PDF APIs don't tag form fields properly for accessibility, and most tagged-PDF APIs don't ship form fields at all. We have both. That combination — fillable + PDF/UA-1 tagged + freshly veraPDF-validated — is exactly the differentiation the compliance audience cares about: gov forms, healthcare consent, fintech KYC.

See it in action

A government-style consent form lives in the makesPDF repo at assets/samples/forms/consent-form-dsl.js, with the rendered output at assets/samples/forms/consent-form-render.pdf. That rendered PDF is also one of the fixtures in our per-release veraPDF gate: 153 PDF/A-2A rules + 106 PDF/UA-1 rules, zero failures.

The full reference for the three builders is now on /docs/api under "Fillable forms," and the skill file at /skills/pdf-template-author.md documents them under the Builder DSL Reference so any agent author can use them on the first try.

Out of scope (for now)

/Sig digital-signature fields, Acrobat-style JavaScript field calculations, and radio-button groups (model with a select instead).

uiapiartifacts

Browseable render history

A new /settings/renders page lists every PDF you've rendered in the last 7 days with a one-click download — and the same list is available to API callers at GET /api/v1/artifacts.

If a download missed, a tab closed, or you just want the PDF you rendered ten minutes ago, you no longer have to re-render to get it back.

What's new

/settings/renders lists your recent render artifacts — date, source endpoint (/md, /preview, /render), template name (for stored-template renders), page count, byte size, and how long until the artifact expires. Each row has a Download link that streams the original PDF straight from R2.

The same data is available programmatically at GET /api/v1/artifacts. It accepts kind (filter by source endpoint), templateId (filter to one stored template), and limit / offset. Owner-scoped, paginated, rate-limited at 500 requests / hour. Pair it with the existing GET /api/v1/artifacts/:id/pdf to fetch the bytes.

Why now

We already captured every successful render under an X-Artifact-Id so issue reports could reference it, and the existing /settings/usage page surfaced the metadata for credit auditing. What was missing was the simple "give me the PDF back" affordance — useful for the new "Try it now" button on /ai/chat (which auto-downloads but can race a closing tab), and for API consumers that want to revisit a render without keeping the response body themselves.

Retention

Artifacts are kept for 7 days by default. Filing an issue report against a render extends retention on that artifact to 30 days so we can reproduce the bug. Both numbers are visible on each row.

apix402agentspayments

Pay-per-call PDFs — x402 is live on Base mainnet

Agents and scripts can now hit /api/v1/md and /api/v1/render with a signed USDC payment instead of an account. $0.01 per markdown render, $0.02 per DSL render, settled on Base.

Until now, every makesPDF request needed a Bearer token or a session cookie tied to a user account. That's a fine model for a developer paying a monthly bill, but it's friction for an autonomous agent that just wants to render a PDF and move on. As of today, you can pay per call with an x402 signed USDC payment instead.

What changed

Hit /api/v1/md or /api/v1/render with no auth and you'll get a 402 Payment Required advertising the price ($0.01 for markdown, $0.02 for DSL), the asset (USDC on Base mainnet), and the receiver address. Sign an EIP-3009 TransferWithAuthorization for that exact amount, retry with the signed payload in a PAYMENT-SIGNATURE header, and the route verifies + settles via the x402 facilitator before rendering. Idempotency is handled for you — the same signed payload coming back inside 5 minutes serves the cached PDF without re-settling.

The shape

Wire format is x402 V2: CAIP-2 network IDs (eip155:8453 for Base mainnet), atomic-unit amounts ("10000" = $0.01 USDC), extra: { name: "USDC", version: "2" }. Bearer-token and session callers are unaffected — same code path, same billing, same response. The paid path forces watermark-free output and rate-limits per wallet instead of per caller. Render failures mark the payment for refund; settle failures don't bill at all.

Caveats

x402 callers on /api/v1/render must use the inline-DSL body form ({ dsl, data }) — there's no stored-template ownership without a user account. If you want template persistence, you'll need a regular account; if you just want to render once and disappear, x402 is the path.

What's next

Bazaar should auto-discover the endpoints within hours. We'll be adding x402 mentions to /llms.txt, the skill file, and the OpenAPI spec next so AI agents can self-onboard from the docs. Wallet-claiming for humans (link an x402 wallet to a regular account) is on the roadmap.

Settled tx, payment row, and refund worklist all live in our D1 — we monitor render_status = 'failed' daily for manual refunds while volume is low.

ai-chatapi

Try your real data from the AI chat

When the AI chat renders a preview against pasted JSON, the workspace pane now shows a copy-pasteable two-step API recipe and one-click Save / Try buttons.

The /ai/chat workspace now teaches you the production rendering flow inline. Once the assistant has produced a preview and you've pasted real JSON in the Render data panel, the workspace pane shows a third section under the PDF preview with the exact two API calls your backend would make:

  1. POST /api/v1/templates { dsl, name } to save the template (free).
  2. POST /api/v1/render { templateId, data } to render with your data (1 credit per 10 pages).

The recipe interpolates the model's actual DSL and your actual JSON, with tabs for curl, fetch, and Node so you can paste the snippet straight into your codebase.

A Save template button creates the template under your account; clicking it again after the chat updates the DSL overwrites the same row. Once saved, Try it now — 1 credit runs the billed render path against your data and swaps the watermarked preview for the production PDF in place. The chat is still for authoring; the API is still where production rendering happens.

layoutdsltemplates

Content-aware grid widths for tables

attr.grid now accepts "auto" and "<n>fr" so columns can size to their data instead of a defensive percentage. Header, body and totals rows of one table coordinate automatically.

attr.grid now accepts two new tokens alongside fixed pt and "<n>%":

  • "auto" — sizes the column to the widest content in it.
  • "<n>fr" — shares the surplus left after fixed and auto columns, proportionally. "1fr" and "2fr" split 1:2; pure-fr grids divide the row by fr factor.

Mix freely. ["120pt", "auto", "1fr"] pins the first column, sizes the second to its content, and lets the third absorb whatever's left. Resolution order is fixed → auto → fr; auto cells that overflow the row shrink toward min-content rather than wrapping.

Tables coordinate widths across rows. A table()'s header and every materialized data row resolve their auto/fr columns once across the union of their content, so column edges line up vertically instead of each row sizing itself in isolation.

A new optional row attribute, grid-id, extends that coordination across separate containers — the use case is totals() aligning with the table() above it. table() and totals() now stamp a deterministic grid-id derived from the shared cols array, so totals rows participate in the same group as the table whose columns they should align with. The Amount column lines up whether it's a fixed percentage, an auto sized to data (e.g. a Balance column in a bank statement), or anything in between.

totRow() (the building block behind totals()) was rewritten to span its label across every column except the last, leaving the value in the final column. Labels get all the room they need and never wrap into a narrow numeric column; the value column is what aligns with the table.

Six library templates were tightened to demonstrate the new tokens — invoice-compact, receipt-retail, quote-modern, statement-bank, shipping-label-standard, and report-standard — with defensive percentages replaced by auto/1fr where they were previously hiding worst-case data-width guesses.

The DSL skill file at /skills/pdf-template-author.md has the new tokens documented in the grid section. The /api/v1/preview and /api/v1/render pipelines pick this up automatically — no API changes.

fontsdocs

BYO-font skill and variable-font guardrail

A dedicated pdf-fonts skill documents per-request and account-level font registration, including the BYO path for non-Latin European scripts. Variable fonts now fail fast with a clear error.

The font story is now properly written down.

  • New skill file: skills/pdf-fonts.md — covers the embedded families (Inter, NotoSans, Cousine, NotoSymbols2), per-request fonts: registration, account-level uploads, and the BYO path for non-Latin European scripts (Cyrillic, Greek, Vietnamese, Georgian, Armenian).
  • The companion-skills index in the main authoring skill points at it, so agents discover it on the first authoring loop.
  • Variable fonts uploaded via /api/v1/fonts are now rejected with an explicit error rather than silently producing garbled output.

CJK and complex-script shaping (Arabic, Hebrew, Indic, Thai, Khmer) are still tracked separately and don't render correctly yet — see the i18n todo docs.

dslbuilderlayout

Smarter column-width defaults for table()

Omit widths in table() and the builder picks for you — every column sizes to content, and the column with the widest measured content automatically absorbs leftover space.

The table() builder used to require an explicit width on every column. In practice that meant authors guessed defensive percentages ("10%", "15%") for numeric columns and quietly hit the same trap whenever a real currency value was wider than the slot — the cell wrapped to two lines and the visible text dropped to the bottom of the row.

Widths are now optional. Omit them and the builder picks:

table(
  [["Description"], ["Qty", undefined, "right"], ["Amount", undefined, "right"]],
  "item in items",
  [["{{item.desc}}"], ["{{item.qty}}"], ["{{item.amount}}"]]
);

Every column starts as auto (sized to its widest measured content). After measurement, the single column with the largest aggregate content width is promoted to 1fr so it absorbs leftover space — usually whichever column carries the long free-form text. Tie-break is leftmost.

The promotion is purely additive. If you write a width — "1fr", "<n>%", <n>pt, or a number — for any column, the builder leaves your array alone. Existing templates render exactly as before.

A new grid token, auto-stretch, makes the candidate-then-promote behaviour available directly in attr.grid for hand-built rows that don't go through table(). Cell widths in the second table() argument are now optional too — the row's grid slot pins the column width, so per-cell widths were always redundant.

Skill: see pdf-template-author.md §Grid token forms.

aibenchmark

AI leaderboard with five scenarios

The /ai page is now a sortable leaderboard across five scenarios and 16+ providers, with per-row download links to every generated PDF.

The bring-your-own-AI benchmark on /ai grew up.

  • 5 scenarios instead of one — invoice, receipt, quote, resume, report.
  • 16+ providers, including OpenRouter routes for cross-vendor comparison.
  • Sortable leaderboard — sort by latency, tokens, success rate, or scenario.
  • Per-row PDF links — every generated artifact is downloadable so you can eyeball the actual output, not just the numeric score.

The benchmark runner is still the same scripts/tools/benchmark-models.ts; results are published as static JSON in the public dir and refreshed on demand.

fontsapi

Account-level font uploads

Upload TTF or OTF fonts once at the account level and reference them by family name from any template — no per-request `fonts:` payload required.

You can now upload custom fonts once at the account level and use them across every template, without re-sending the bytes on every render.

  • Upload at /settings/fonts — TTF or OTF, max 5 MB per variant.
  • Variable fonts are rejected with a clear error (we don't support them yet).
  • Up to 4 families per account, 6 variants per family.
  • Fonts are subsetted on registration and cached in KV.

Once uploaded, just reference the family in your DSL styles:

text({ "font-family": "Brand", "font-weight": "bold" }, "Hello");

The per-request fonts: payload on /preview and /render still works for one-off cases, but accounts that use the same brand fonts everywhere don't have to ship them on each call any more.

This is also the supported path for non-Latin European scripts (Cyrillic, Greek, Vietnamese, Georgian, Armenian) — see the pdf-fonts skill.

billingadmin

Custom-amount Growth purchase + admin credit grant

Top up between $20 and $500 in a single Growth purchase, and admins can now grant wallet credits directly to any user.

Two billing improvements landed together.

Custom-amount Growth purchase

The Growth plan now accepts any amount between $20 and $500 in a single purchase. Pick a number, hit checkout, credits land in your wallet.

Admin credit grants

Admins can grant wallet credits directly to any user — handy for support cases, pilot programs, and one-off comps without round-tripping through Stripe.

A few smaller polish fixes along the way: plan-card CTA buttons align consistently at the bottom of each card, the checkout spinner clears properly after a Stripe redirect, and the makespdf.com watermark on free renders is now anchored to the page bottom.

blogmermaidarchitecture

Blog subdomain, plus a clean Mermaid renderer

blog.makespdf.com is live, served from the same Worker. Under the hood, the Mermaid pipeline was rewritten as @pdf/mermaid-render — 80% smaller SVG, no fake DOM, Workers-native.

Two things shipped together because they wanted each other.

blog.makespdf.com

The blog is now served from a dedicated subdomain with its own routing, while still living inside the same Cloudflare Worker as the rest of the app. Posts are markdown files in apps/web/content/blog/; each one is also available as a download-ready PDF rendered by the same engine that powers the public API.

@pdf/mermaid-render

The Mermaid → SVG pipeline was rewritten end-to-end. The old approach pulled in mermaid + linkedom (76.5 MB installed) and depended on a fake DOM; the new package is a clean parse → layout → render pipeline:

  • Vendored jison parsers from Mermaid v11.12.2 produce typed ASTs.
  • ELK.js computes graph layout for flowchart / class / state / ER.
  • SVG XML is generated directly — no DOM, no shim.
  • Single external dependency: elkjs.

Result: 7.7 MB installed (down from 76.5 MB), SVG output ~80% smaller (3 KB vs 15 KB), deterministic, Workers-compatible. Supports all 8 Mermaid diagram types: flowchart (with subgraphs), sequence, class, state, gantt, pie, ER, gitgraph.

apilibrary

Template library exposed to API agents

The curated template library (invoices, receipts, quotes, resumes, …) is now browsable over the API. Discover a template, fetch its DSL, fork into a user template, render.

The curated template library — invoices, receipts, quotes, resumes, and friends — is now first-class over the API.

GET /api/v1/library                # list / search (category, tags, q)
GET /api/v1/library/:id            # fetch a template's full DSL + sampleData

Both endpoints are read-only and rate-limited at 500 requests/hour.

There is intentionally no "render a library template directly" shortcut. The flow is fork-then-render:

  1. GET /api/v1/library?category=invoice&q=classic — discover.
  2. GET /api/v1/library/invoice-classic — fetch DSL.
  3. Customise locally, then POST /api/v1/templates to create a user copy.
  4. POST /api/v1/render { templateId, data } as normal.

This keeps ownership and billing attached to a concrete user row.

apimcpai

MCP authoring-loop server

A first-party Model Context Protocol server at /api/v1/mcp lets agents drive the full authoring loop — preview → save → render — without scraping HTML.

makesPDF now exposes a Model Context Protocol server at /api/v1/mcp. Agents (Claude Desktop, Claude Code, Cursor, anything that speaks MCP) can drive the full authoring loop without scraping HTML or hand-rolling a client.

The server exposes tools for:

  • Preview — render inline DSL with sample data, zero credits.
  • Save / list / fetch / update / delete templates — the full persistence API.
  • Render — produce a real PDF from a saved template.
  • Library browsing — discover the curated template library and fetch DSL to fork.

Setup instructions: see /docs/mcp. The server is authed the same way as the rest of /api/v1/* — Bearer API key or session cookie.

apireports

Render artifacts and issue reports

Every render is now captured as an artifact (inputs + output) addressable by an X-Artifact-Id header, with user and admin pages to file and triage rendering issues.

Every successful /render and /preview call now captures the input DSL, the input data, and the resulting PDF as a first-class artifact. The response carries an X-Artifact-Id header so callers can refer to a specific render later.

The Done panel after a render in the web UI now exposes:

  • A direct download link for the finished PDF.
  • A "Report an issue" hook that ties the report to the artifact id — no need to re-upload anything.

Two new pages were added:

  • /settings/reports — your own submitted reports.
  • /admin/reports — admin triage view across all users.

A nightly cleanup job sweeps expired render artifacts so storage stays bounded.

apibreakingbilling

Draft / publish split — and AI moves out of the core

The biggest pivot since launch. /api/v1/render now takes a templateId you saved earlier, /preview is the free authoring surface, and AI is no longer part of the core worker.

The biggest pivot since launch. makesPDF is now positioned as the fastest PDF rendering API — no longer an AI product. The AI is yours to bring.

Breaking: /api/v1/render contract changed

  • Old: POST /api/v1/render { type, data } — called Claude/OpenAI to auto-generate a template, then rendered it. Billed.
  • New: POST /api/v1/render { templateId, data } — renders a template you previously saved via POST /api/v1/templates. Billed (1 credit / 10 pages on success; zero on any 4xx/5xx).

There is no migration shim — pre-launch, no external customers to migrate.

New: template persistence API

  • POST /api/v1/templates — save a DSL template (free; catalog-validated; 512KB DSL cap).
  • GET /api/v1/templates — list your templates.
  • GET /api/v1/templates/:id — fetch DSL.
  • PUT /api/v1/templates/:id — update DSL or name.
  • DELETE /api/v1/templates/:id — delete.

All endpoints user-scoped; 404 for unknown or non-owned ids (indistinguishable by design).

/api/v1/preview is now the free draft surface

Iterate on DSL with /preview (free, length-preserving filler in data, "PREVIEW — NOT FOR USE" watermark baked in), save with /templates, then render real PDFs with /render.

AI removed from the core worker

CLAUDE_API_KEY is no longer required for deployment. It stays optional for dev tooling only (scan-template.ts, batch-scan.ts, the bakeoff runner). No Anthropic/OpenAI SDK is loaded at request time.

Promo: /api/v1/md free under a page cap

KV-gated. The Markdown-to-PDF route skips the credit deduct when pages ≤ cap and emits an X-Credits-Free-Under header.