API Reference

The Printmint base URL is https://api-printmint.plxi.us. Every endpoint accepts and returns JSON and is authenticated with your API key.

curl -X POST https://api-printmint.plxi.us/v1/pdf/render \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "css": "body { font-family: Inter, Arial, sans-serif; padding: 20mm; } h1 { color: #10b981; }",
  "html": "<html><head><title>Hello</title></head><body><h1>Hello from Printmint</h1><p>This page was printed from HTML.</p></body></html>",
  "inline": true,
  "pdf": {
    "footer_html": "<div style=\"width:100%;font-size:8px;text-align:center\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>",
    "margin": {
      "bottom": "15mm"
    },
    "page_size": "A4"
  }
}'
New accounts include 100 free documents per month, no card required. Create an account and grab your key from the dashboard.

Authentication

Send an X-API-Key header with every request. Keys are created and revoked in the dashboard; a key is shown once, when it is created, so store it somewhere safe.

X-API-Key: sk_live_...
Keep keys on your server. Anyone with your key can spend your quota, so never put it in browser JavaScript or a public repository.

Documents & templates

Printmint prints with Chromium, as Chrome prints a page: your CSS, web fonts, @page rules and page breaks all apply. Each PDF or image costs one credit, up to 50 pages.

Getting the file back

A result carries the document as a download link in file (it expires at expires_at, and the file is deleted a few days later), or in the response as base64: pdf_base64 or image_base64. Send "inline": true to always get it in the response; inline documents are at most 5 MB. When the service isn’t storing files, every result comes back inline.

{
  "pages": 2,
  "bytes": 48213,
  "content_type": "application/pdf",
  "file": {
    "url": "https://…/invoice.pdf?X-Amz-Signature=…",
    "expires_at": "2026-11-01T12:00:00Z",
    "bytes": 48213,
    "sha256": "9f2c…",
    "content_type": "application/pdf"
  },
  "credits": 1
}

Page setup

pdf sets the paper (page_size, A4 by default, or width and height such as "4in"), margin, landscape and page_ranges. header_html and footer_html repeat on every page inside its margin; elements with the classes pageNumber, totalPages, date and title are filled in. Give them inline styles and a margin to sit in.

Templates

POST /v1/pdf/template fills in a template with your data. Five are built in: invoice, receipt, certificate, report and shipping_label. POST /v1/templates/list returns each one’s fields and sample data. Your own templates use Jinja syntax and are saved with POST /v1/templates/create; you print them by their id. A team’s keys share the owner’s templates.

<h1>Invoice {{ number }}</h1>
<p>Issued {{ issued|date }}</p>            {# 31 October 2026 #}
<table>
{% for item in items %}
  <tr><td>{{ item.description }}</td>
      <td>{{ (item.quantity * item.unit_price)|money(currency) }}</td></tr>
{% endfor %}
</table>
{% if notes %}<p>{{ notes }}</p>{% endif %}
  • Every value is HTML-escaped, so data prints as text. Use |safe only for HTML you trust.
  • A field the template prints but data lacks is a 400 missing_field with its line, not a blank. Test optional fields with {% if notes %} or give a default: {{ notes|default('') }}.
  • Filters: money("EUR") (€1,234.50), number(2) (1,234.50) and date("%d/%m/%Y") for YYYY-MM-DD dates, besides Jinja’s own.
  • Templates can’t include or import other templates, read files or reach the network, and a render that runs too long stops with template_too_slow.

Network

HTML you send may load images, stylesheets and fonts from public URLs; base_url resolves relative ones. Templates print with offline on, loading nothing, so give them images as data: URLs or send "offline": false. Pages never reach private addresses.

Limits

  • HTML: 2 MB. A saved template: 512 KB, and 100 templates per account.
  • One PDF: 50 pages and 60 seconds to print. A merge: 20 PDFs of up to 10 MB each, 1,000 pages.
  • Images: up to 3840 × 2160 at scale 1 to 3; a full page up to 16,000 px tall.

Plans & limits

Limits apply per account, not per key: every key on the account (and teammates working in it) shares them. There is a per-minute rate, a cap on requests in flight at once, and a monthly quota. On the Free plan the quota resets on the 1st of each month (UTC); on paid plans it resets on your billing-period anniversary. Requests that fail validation are never charged.

PlanPriceRequests / minIn flightDocuments / month
FreeFree101100
Starter$10/mo6033,000
Pro$50/mo300630,000

Errors

Errors return JSON with a stable error code, an optional message, and, on rate-limit and monthly-quota 429s, reset_at. A request that returns an error is never charged.

{
  "error": "rate_limit_exceeded",
  "reset_at": "2026-07-22T18:04:00Z"
}

Fix the request

StatusCodeWhenWhat to do
400
bad_request
The body is not valid JSON.Send the body as JSON.
400
invalid_input
A body field failed the operation’s checks; message says which one and why.Correct that field.
401
invalid_api_key
The X-API-Key header is missing, malformed, revoked or not a key we issued.Check the key, or create a new one in the dashboard.
403
account_blocked
The account is suspended.Contact [email protected].
404
not_found
No endpoint at that path for that method: operations take POST, /v1/usage takes GET.Check the path and the method.
413
payload_too_large
The body is larger than the endpoint takes: 1 MiB unless it says otherwise.Send a smaller body.
400
missing_field
template_syntax
template_error
template_include
template_too_slow
A template couldn’t be filled in: data lacks a field it prints, or the template itself is wrong. message and line say where.Add the field to data (list_templates shows each template’s fields), or fix the template.
400
document_too_large
invalid_pdf
too_many_pages
The HTML is over 2 MB, a merge source isn’t a readable PDF (or is encrypted), or a merge would pass 1,000 pages.Send less, or the right file.
404
template_not_found
template is neither a built-in name nor one of your templates’ ids.Call list_templates (POST /v1/templates/list) for the names and ids.
413
result_too_large
The document is over 5 MB and was asked for inline.Leave out "inline", or make the document smaller.
422
too_many_pages
unrenderable_url
page_too_large
The document printed to more than 50 pages, or the page couldn’t be loaded.Split it, or print part with pdf.page_ranges.

Wait, then retry

StatusCodeWhenWhat to do
429
rate_limit_exceeded
The per-minute request limit is reached (per account, across all its keys). Requests that then fail, for example on invalid input, count toward it too.Retry after reset_at, the start of the next minute.
429
concurrency_limit_exceeded
The account has as many requests in flight as its plan allows.Retry with backoff once one of them finishes. There is no reset_at.
429
monthly_quota_exceeded
The quota for this billing period is used up.Retry after reset_at: the 1st of next month (UTC) on Free, the end of your billing period on paid plans. Or upgrade.

Temporary: retry with backoff

StatusCodeWhenWhat to do
400
body_read_failed
The connection failed while the body was being sent.Retry.
408
request_body_timeout
The body took too long to arrive.Retry, sending the whole body at once.
500
internal_error
Something failed on our side.Retry with backoff.
503
capacity_unavailable
The API is handling as many requests as it can.Retry with backoff.
503
database_unavailable
limits_unavailable
Our database, or the store that counts rate limits, is unavailable.Retry with backoff.
503
auth_busy
API key checks are busy.Retry with backoff.
503
billing_period_stale
billing_period_invalid
On a paid plan, the current billing period can’t be confirmed, for example because a renewal hasn’t been recorded yet.Retry later. If it lasts, the account owner can open Billing in the dashboard to refresh it, or contact [email protected].
504
request_timeout
The operation ran past the API’s time limit.Retry with backoff; if it keeps happening, send smaller input.
A response without a JSON body comes from the proxies in front of the API, for example a 413 for a body far over the limit or a 502 while the API restarts. Retry the 5xx ones with backoff.

Use with AI agents (MCP)

Printmint is also an MCP server, so Claude and other AI agents can call its main operations as tools (listed below). Tool calls are charged exactly like API calls, to the same plan and quota, and failed calls are free.

https://api-printmint.plxi.us/mcp

Claude (claude.ai and the desktop app): Settings → Connectors → Add custom connector, then paste the URL above. Claude asks you to sign in with your Plexsicity account; calls are then charged to your own account.

Claude Code, Cursor, VS Code and other clients: send an API key from the dashboard as a header. Calls are charged to the account that owns the key, so a team’s key spends the team’s quota. Clients that only take a bearer token can send Authorization: Bearer sk_live_… instead.

1. Connect your agent

claude mcp add --transport http printmint https://api-printmint.plxi.us/mcp \
  --header "X-API-Key: sk_live_..."

2. Tell it when to use the tools

Add this to the repository’s AGENTS.md or CLAUDE.md. Agents also get these rules from the server when they connect; the file makes them part of the project.

## Printmint
- To make a PDF from HTML, call `html_to_pdf`. For an invoice, receipt, certificate, report or shipping label, call `list_templates`, then `render_template_pdf` with JSON data, instead of writing the HTML yourself.
- To make an image (a social card, a badge, a certificate as a PNG) from HTML or a template, call `html_to_image`.
- To join PDFs into one, call `merge_pdfs`.
Tools: merge_pdfs, html_to_pdf, render_template_pdf, html_to_image, list_templates. Tools take URLs, not file uploads. Results are JSON, also returned as structured content.

POST/v1/pdf/merge

Join 2 to 20 PDFs into one, in order. Costs 1 document.

FieldTypeDescription
inline?booleanReturn the file in the response as base64 (up to 5 MB) instead of a download link.
sourcesarrayRequired. The PDFs, in order.
Example request body
{
  "inline": true,
  "sources": [
    {
      "pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooQ292ZXIgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
    },
    {
      "pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooVGVybXMgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
    }
  ]
}
Example response · 200
{
  "bytes": 1024,
  "content_type": "application/pdf",
  "credits": 1,
  "pages": 2,
  "pdf_base64": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29kZT4+..."
}
curl -X POST https://api-printmint.plxi.us/v1/pdf/merge \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "inline": true,
  "sources": [
    {
      "pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooQ292ZXIgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
    },
    {
      "pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooVGVybXMgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
    }
  ]
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: merge_pdfs.

POST/v1/pdf/render

Print HTML to a PDF, with page size, margins, headers and footers. Costs 1 document.

FieldTypeDescription
base_url?stringWhere relative links, images and stylesheets in the HTML resolve from.
css?stringCSS added to the document's head.
delay_ms?integerThen wait this many milliseconds more.
htmlstringRequired. The HTML document to print.
inline?booleanReturn the file in the response as base64 (up to 5 MB) instead of a download link.
javascript?booleanRun the page's scripts (on by default), for charts and other HTML built by script.
offline?booleanLoad nothing from the network: images and fonts must be in the HTML (data: URLs). Off by default.
pdf?objectHow the PDF is laid out. Every field is optional.
wait?"load" | "domcontentloaded" | "networkidle"When the page is ready to print: load (default), domcontentloaded, or networkidle (no requests for half a second).
wait_for_selector?stringAlso wait until an element matching this CSS selector appears.
Example request body
{
  "css": "body { font-family: Inter, Arial, sans-serif; padding: 20mm; } h1 { color: #10b981; }",
  "html": "<html><head><title>Hello</title></head><body><h1>Hello from Printmint</h1><p>This page was printed from HTML.</p></body></html>",
  "inline": true,
  "pdf": {
    "footer_html": "<div style=\"width:100%;font-size:8px;text-align:center\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>",
    "margin": {
      "bottom": "15mm"
    },
    "page_size": "A4"
  }
}
Example response · 200
{
  "bytes": 18342,
  "content_type": "application/pdf",
  "credits": 1,
  "pages": 1,
  "pdf_base64": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29kZT4+..."
}
curl -X POST https://api-printmint.plxi.us/v1/pdf/render \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "css": "body { font-family: Inter, Arial, sans-serif; padding: 20mm; } h1 { color: #10b981; }",
  "html": "<html><head><title>Hello</title></head><body><h1>Hello from Printmint</h1><p>This page was printed from HTML.</p></body></html>",
  "inline": true,
  "pdf": {
    "footer_html": "<div style=\"width:100%;font-size:8px;text-align:center\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>",
    "margin": {
      "bottom": "15mm"
    },
    "page_size": "A4"
  }
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: html_to_pdf.

POST/v1/pdf/template

Fill in a template with JSON data and print it as a PDF. Costs 1 document.

FieldTypeDescription
base_url?stringWhere relative links, images and stylesheets in the HTML resolve from.
css?stringCSS added to the document's head.
data?objectThe values the template fills in. list_templates shows each template's fields and a sample.
delay_ms?integerThen wait this many milliseconds more.
inline?booleanReturn the file in the response as base64 (up to 5 MB) instead of a download link.
javascript?booleanRun the page's scripts (on by default), for charts and other HTML built by script.
offline?booleanLoad nothing from the network: images and fonts must be in the HTML (data: URLs). On by default for templates.
pdf?objectHow the PDF is laid out. Every field is optional.
templatestringRequired. A built-in template (invoice, receipt, certificate, report, shipping_label) or the id of one of your own.
wait?"load" | "domcontentloaded" | "networkidle"When the page is ready to print: load (default), domcontentloaded, or networkidle (no requests for half a second).
wait_for_selector?stringAlso wait until an element matching this CSS selector appears.
Example request body
{
  "data": {
    "currency": "USD",
    "customer": {
      "address": [
        "400 Market Street, Suite 12",
        "San Francisco, CA 94105"
      ],
      "email": "[email protected]",
      "name": "Acme Robotics Inc."
    },
    "due": "2026-11-30",
    "issued": "2026-10-31",
    "items": [
      {
        "description": "Website redesign: discovery and wireframes",
        "quantity": 1,
        "unit_price": 2400
      },
      {
        "description": "Design system components",
        "quantity": 12,
        "unit_price": 150
      },
      {
        "description": "Hosting, October",
        "quantity": 1,
        "unit_price": 49.5
      }
    ],
    "notes": "Thank you for your business. Please pay by bank transfer within 30 days and quote the invoice number.",
    "number": "INV-2026-0142",
    "payment": [
      {
        "label": "Bank",
        "value": "First National Bank"
      },
      {
        "label": "Account name",
        "value": "Northwind Studio LLC"
      },
      {
        "label": "Account",
        "value": "1234 5678"
      },
      {
        "label": "Routing",
        "value": "021000021"
      }
    ],
    "seller": {
      "address": [
        "18 Harbour Street",
        "Portland, OR 97204",
        "United States"
      ],
      "email": "[email protected]",
      "name": "Northwind Studio",
      "tax_id": "12-3456789"
    },
    "tax_rate": 0.0875
  },
  "inline": true,
  "template": "invoice"
}
Example response · 200
{
  "bytes": 41207,
  "content_type": "application/pdf",
  "credits": 1,
  "pages": 1,
  "pdf_base64": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29kZT4+...",
  "template": "invoice"
}
curl -X POST https://api-printmint.plxi.us/v1/pdf/template \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "data": {
    "currency": "USD",
    "customer": {
      "address": [
        "400 Market Street, Suite 12",
        "San Francisco, CA 94105"
      ],
      "email": "[email protected]",
      "name": "Acme Robotics Inc."
    },
    "due": "2026-11-30",
    "issued": "2026-10-31",
    "items": [
      {
        "description": "Website redesign: discovery and wireframes",
        "quantity": 1,
        "unit_price": 2400
      },
      {
        "description": "Design system components",
        "quantity": 12,
        "unit_price": 150
      },
      {
        "description": "Hosting, October",
        "quantity": 1,
        "unit_price": 49.5
      }
    ],
    "notes": "Thank you for your business. Please pay by bank transfer within 30 days and quote the invoice number.",
    "number": "INV-2026-0142",
    "payment": [
      {
        "label": "Bank",
        "value": "First National Bank"
      },
      {
        "label": "Account name",
        "value": "Northwind Studio LLC"
      },
      {
        "label": "Account",
        "value": "1234 5678"
      },
      {
        "label": "Routing",
        "value": "021000021"
      }
    ],
    "seller": {
      "address": [
        "18 Harbour Street",
        "Portland, OR 97204",
        "United States"
      ],
      "email": "[email protected]",
      "name": "Northwind Studio",
      "tax_id": "12-3456789"
    },
    "tax_rate": 0.0875
  },
  "inline": true,
  "template": "invoice"
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: render_template_pdf.

POST/v1/image/render

Draw HTML or a template as a PNG, JPEG or WebP image. Costs 1 document.

FieldTypeDescription
base_url?stringWhere relative links, images and stylesheets in the HTML resolve from.
css?stringCSS added to the document's head.
dark_mode?booleanPrefer the page's dark colour scheme.
data?objectThe values the template fills in. list_templates shows each template's fields and a sample.
delay_ms?integerThen wait this many milliseconds more.
format?"png" | "jpeg" | "webp"Image format (default png).
full_page?booleanThe whole page, not just the viewport (up to 16,000 px tall).
height?integerViewport height in CSS pixels (default 630).
html?stringThe HTML to draw.
inline?booleanReturn the file in the response as base64 (up to 5 MB) instead of a download link.
javascript?booleanRun the page's scripts (on by default), for charts and other HTML built by script.
offline?booleanLoad nothing from the network: images and fonts must be in the HTML (data: URLs). Off by default.
quality?integerJPEG or WebP quality (default 85).
scale?integerDevice pixel ratio, 1 to 3.
selector?stringCapture only the first element matching this CSS selector.
template?stringA built-in template (invoice, receipt, certificate, report, shipping_label) or the id of one of your own.
wait?"load" | "domcontentloaded" | "networkidle"When the page is ready to print: load (default), domcontentloaded, or networkidle (no requests for half a second).
wait_for_selector?stringAlso wait until an element matching this CSS selector appears.
width?integerViewport width in CSS pixels (default 1200).
Example request body
{
  "format": "png",
  "height": 630,
  "html": "<body style=\"margin:0;display:flex;align-items:center;justify-content:center;height:100vh;background:#10b981;color:#fff;font:700 64px Inter,Arial,sans-serif\">Printmint</body>",
  "inline": true,
  "width": 1200
}
Example response · 200
{
  "bytes": 21877,
  "content_type": "image/png",
  "credits": 1,
  "height": 630,
  "image_base64": "iVBORw0KGgoAAAANSUhEUgAABLAAAAJ2CAIAAAD...",
  "width": 1200
}
curl -X POST https://api-printmint.plxi.us/v1/image/render \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "format": "png",
  "height": 630,
  "html": "<body style=\"margin:0;display:flex;align-items:center;justify-content:center;height:100vh;background:#10b981;color:#fff;font:700 64px Inter,Arial,sans-serif\">Printmint</body>",
  "inline": true,
  "width": 1200
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: html_to_image.

POST/v1/templates/list

The built-in templates and your own, with their fields and sample data. Costs 0 documents.

FieldTypeDescription
include_html?booleanInclude each template's HTML.
Example request body
{}
Example response · 200
{
  "credits": 0,
  "starters": [
    {
      "description": "An itemised invoice…",
      "fields": {
        "number": "Invoice number"
      },
      "name": "invoice",
      "pdf": {
        "page_size": "A4"
      },
      "sample": {
        "number": "INV-2026-0142"
      },
      "title": "Invoice"
    }
  ],
  "templates": []
}
curl -X POST https://api-printmint.plxi.us/v1/templates/list \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: list_templates.

POST/v1/templates/create

Save a template of your own: Jinja HTML, CSS, sample data and page setup. Costs 0 documents.

FieldTypeDescription
css?stringCSS added to its head.
description?string
htmlstringRequired. The template: HTML with Jinja tags.
namestringRequired. A name for the template.
pdf?objectHow the PDF is laid out. Every field is optional.
sample?objectData to fill it in with in the Playground, and to check it with now.
Example request body
{
  "css": "body { font-family: Inter, Arial, sans-serif; padding: 25mm; }",
  "html": "<h1>Thank you, {{ name }}!</h1><p>Your order {{ order }} is on its way.</p>",
  "name": "Thank-you note",
  "pdf": {
    "page_size": "A5"
  },
  "sample": {
    "name": "Sam",
    "order": "#1042"
  }
}
Example response · 200
{
  "created_at": "2026-10-31T12:00:00Z",
  "credits": 0,
  "description": null,
  "id": "0b8f6a4e-3c2d-4c1a-9f0e-6d5b4a3c2b1a",
  "name": "Thank-you note",
  "pdf": {
    "page_size": "A5"
  },
  "sample": {
    "name": "Sam",
    "order": "#1042"
  },
  "updated_at": "2026-10-31T12:00:00Z"
}
curl -X POST https://api-printmint.plxi.us/v1/templates/create \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "css": "body { font-family: Inter, Arial, sans-serif; padding: 25mm; }",
  "html": "<h1>Thank you, {{ name }}!</h1><p>Your order {{ order }} is on its way.</p>",
  "name": "Thank-you note",
  "pdf": {
    "page_size": "A5"
  },
  "sample": {
    "name": "Sam",
    "order": "#1042"
  }
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. Not an MCP tool: it is for the dashboard and your own scripts.

POST/v1/templates/update

Change a saved template; fields you leave out stay as they are. Costs 0 documents.

FieldTypeDescription
css?string | null
description?string | null
html?string
idstringRequired. The template's id.
name?string
pdf?any
sample?object | null
Example request body
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "Thank-you note (v2)"
}
Example response · 200
{
  "created_at": "2026-10-31T12:00:00Z",
  "credits": 0,
  "description": null,
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "Thank-you note (v2)",
  "pdf": {
    "page_size": "A5"
  },
  "sample": {
    "name": "Sam",
    "order": "#1042"
  },
  "updated_at": "2026-10-31T12:05:00Z"
}
curl -X POST https://api-printmint.plxi.us/v1/templates/update \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "Thank-you note (v2)"
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. Not an MCP tool: it is for the dashboard and your own scripts.

POST/v1/templates/delete

Delete a saved template. Costs 0 documents.

FieldTypeDescription
idstringRequired. The template's id.
Example request body
{
  "id": "00000000-0000-4000-8000-000000000000"
}
Example response · 200
{
  "credits": 0,
  "deleted": true,
  "id": "00000000-0000-4000-8000-000000000000"
}
curl -X POST https://api-printmint.plxi.us/v1/templates/delete \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "id": "00000000-0000-4000-8000-000000000000"
}'

The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. Not an MCP tool: it is for the dashboard and your own scripts.

GET/v1/usage

Your plan and usage this billing period, for alerting before you reach the quota.

curl https://api-printmint.plxi.us/v1/usage \
  -H "X-API-Key: sk_live_..."
Response 200
{
  "plan": "starter",
  "used": 487,
  "limit": 3000,
  "period_start": "2026-07-18T09:12:44Z",
  "period_end": "2026-08-18T09:12:44Z"
}

Webhooks

Add an HTTPS endpoint under Webhooks in the dashboard and Printmint POSTs a request.completed event to it after each charged request. Each endpoint gets its own signing secret (whsec_…), shown once when you add it.

POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Printmint-Webhooks/1
X-Printmint-Event: request.completed
X-Printmint-Event-ID: 6f1c2a7e-…:0d4e9b2c-…
X-Printmint-Signature: sha256=3b5f0c9e41d7…
X-Printmint-Webhook-Signature: t=1790690591,v1=9c2e41f07a3d…

{"id": "6f1c2a7e-…:0d4e9b2c-…", "data": {"ms": 412, "credits": 1, "endpoint": "pdf.render"}, "event": "request.completed", "created_at": "2026-09-29T14:03:11.482107+00:00"}
Verify the signature

X-Printmint-Webhook-Signature is t=<unix seconds>,v1=<hex>: the HMAC-SHA256 of <t>.<event id>.<raw body>, keyed with the endpoint’s full secret including whsec_. Compute it over the bytes you received, before parsing, and refuse a timestamp more than 5 minutes old. The older X-Printmint-Signature (sha256= of the body alone) is still sent while integrations move over, and will be removed after notice; it can’t stop a replayed delivery.

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (Buffer or string). Verify it
// before parsing -- JSON that has been parsed and re-serialised won't match.
// secret: the endpoint's full signing secret, "whsec_" prefix included.
// The header is "t=<unix seconds>,v1=<hex>": the HMAC-SHA256 of
// "<t>.<event id>.<body>". One more than 5 minutes old is refused, so a
// captured delivery can't be replayed.
function verifySignature(rawBody, header, eventId, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(t + '.' + eventId + '.').update(rawBody).digest('hex');
  const given = Buffer.from(String(parts.v1 || ''));
  const want = Buffer.from(expected);
  return given.length === want.length && crypto.timingSafeEqual(given, want);
}

// Express: take this route's body raw, verify, then parse.
app.post('/your/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const id = req.get('X-Printmint-Event-ID');
  const ok = verifySignature(req.body, req.get('X-Printmint-Webhook-Signature'), id, process.env.WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(401);
  // "id" in the body is the same as the header: a retry sends it again, so skip one already handled.
  const { event, data } = JSON.parse(req.body);
  res.sendStatus(200);
});
Reply with any 2xx within 8 seconds. Anything else, including a redirect, a timeout or a connection error, is retried after 1, 2, 4, 8, 16 and 32 minutes, then 1 hour: 8 attempts over about two hours. Every attempt carries the same X-Printmint-Event-ID, so use it to ignore duplicates. Recent attempts are listed under Deliveries on the Webhooks page. Endpoints must be public HTTPS URLs.