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"
}
}'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_...
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
|safeonly for HTML you trust. - A field the template prints but
datalacks is a 400missing_fieldwith 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) anddate("%d/%m/%Y")forYYYY-MM-DDdates, 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.
| Plan | Price | Requests / min | In flight | Documents / month |
|---|---|---|---|---|
| Free | Free | 10 | 1 | 100 |
| Starter | $10/mo | 60 | 3 | 3,000 |
| Pro | $50/mo | 300 | 6 | 30,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
| Status | Code | When | What 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
| Status | Code | When | What 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
| Status | Code | When | What 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. |
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`.
POST/v1/pdf/merge
Join 2 to 20 PDFs into one, in order. Costs 1 document.
| Field | Type | Description |
|---|---|---|
| inline? | boolean | Return the file in the response as base64 (up to 5 MB) instead of a download link. |
| sources | array | Required. The PDFs, in order. |
{
"inline": true,
"sources": [
{
"pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooQ292ZXIgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
},
{
"pdf_base64": "JVBERi0xLjcKJbutwN4KMSAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNSAwIFJdL0NvdW50IDEvUmVzb3VyY2VzIDMgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagoyIDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iagozIDAgb2JqCjw8L0ZvbnQ8PC9GMSAyIDAgUj4+Pj4KZW5kb2JqCjQgMCBvYmoKPDwvTGVuZ3RoIDQzPj5zdHJlYW0KQlQKL0YxIDI0IFRmCjcyIDcyMCBUZAooVGVybXMgcGFnZSAxKSBUagpFVAplbmRzdHJlYW0gCmVuZG9iago1IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMSAwIFIvQ29udGVudHMgNCAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9Sb290IDYgMCBSL1R5cGUvWFJlZi9TaXplIDgvV1sxIDQgMl0vSW5kZXhbMSA3XS9MZW5ndGggNDk+PnN0cmVhbQoBAAAADwAAAQAAAGgAAAEAAACnAAABAAAAzQAAAQAAASgAAAEAAAFiAAABAAABjwAACmVuZHN0cmVhbSAKZW5kb2JqCgpzdGFydHhyZWYKMzk5CiUlRU9G"
}
]
}{
"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.
| Field | Type | Description |
|---|---|---|
| base_url? | string | Where relative links, images and stylesheets in the HTML resolve from. |
| css? | string | CSS added to the document's head. |
| delay_ms? | integer | Then wait this many milliseconds more. |
| html | string | Required. The HTML document to print. |
| inline? | boolean | Return the file in the response as base64 (up to 5 MB) instead of a download link. |
| javascript? | boolean | Run the page's scripts (on by default), for charts and other HTML built by script. |
| offline? | boolean | Load nothing from the network: images and fonts must be in the HTML (data: URLs). Off by default. |
| pdf? | object | How 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? | string | Also wait until an element matching this CSS selector appears. |
{
"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"
}
}{
"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.
| Field | Type | Description |
|---|---|---|
| base_url? | string | Where relative links, images and stylesheets in the HTML resolve from. |
| css? | string | CSS added to the document's head. |
| data? | object | The values the template fills in. list_templates shows each template's fields and a sample. |
| delay_ms? | integer | Then wait this many milliseconds more. |
| inline? | boolean | Return the file in the response as base64 (up to 5 MB) instead of a download link. |
| javascript? | boolean | Run the page's scripts (on by default), for charts and other HTML built by script. |
| offline? | boolean | Load nothing from the network: images and fonts must be in the HTML (data: URLs). On by default for templates. |
| pdf? | object | How the PDF is laid out. Every field is optional. |
| template | string | Required. 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? | string | Also wait until an element matching this CSS selector appears. |
{
"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"
}{
"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.
| Field | Type | Description |
|---|---|---|
| base_url? | string | Where relative links, images and stylesheets in the HTML resolve from. |
| css? | string | CSS added to the document's head. |
| dark_mode? | boolean | Prefer the page's dark colour scheme. |
| data? | object | The values the template fills in. list_templates shows each template's fields and a sample. |
| delay_ms? | integer | Then wait this many milliseconds more. |
| format? | "png" | "jpeg" | "webp" | Image format (default png). |
| full_page? | boolean | The whole page, not just the viewport (up to 16,000 px tall). |
| height? | integer | Viewport height in CSS pixels (default 630). |
| html? | string | The HTML to draw. |
| inline? | boolean | Return the file in the response as base64 (up to 5 MB) instead of a download link. |
| javascript? | boolean | Run the page's scripts (on by default), for charts and other HTML built by script. |
| offline? | boolean | Load nothing from the network: images and fonts must be in the HTML (data: URLs). Off by default. |
| quality? | integer | JPEG or WebP quality (default 85). |
| scale? | integer | Device pixel ratio, 1 to 3. |
| selector? | string | Capture only the first element matching this CSS selector. |
| template? | string | 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? | string | Also wait until an element matching this CSS selector appears. |
| width? | integer | Viewport width in CSS pixels (default 1200). |
{
"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
}{
"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.
| Field | Type | Description |
|---|---|---|
| include_html? | boolean | Include each template's HTML. |
{}{
"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.
| Field | Type | Description |
|---|---|---|
| css? | string | CSS added to its head. |
| description? | string | |
| html | string | Required. The template: HTML with Jinja tags. |
| name | string | Required. A name for the template. |
| pdf? | object | How the PDF is laid out. Every field is optional. |
| sample? | object | Data to fill it in with in the Playground, and to check it with now. |
{
"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"
}
}{
"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.
| Field | Type | Description |
|---|---|---|
| css? | string | null | |
| description? | string | null | |
| html? | string | |
| id | string | Required. The template's id. |
| name? | string | |
| pdf? | any | |
| sample? | object | null |
{
"id": "00000000-0000-4000-8000-000000000000",
"name": "Thank-you note (v2)"
}{
"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.
| Field | Type | Description |
|---|---|---|
| id | string | Required. The template's id. |
{
"id": "00000000-0000-4000-8000-000000000000"
}{
"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_..."{
"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"}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);
});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.