Documentation

REST API · Render

The public REST API renders Markdown into themed PDFs and manages your templates. It's a server-to-server API authenticated by an API key.

  • Base URL: https://inkrun.dev.
  • Timing: a render is synchronous and can take up to ~60s.
  • CORS: cross-origin browser reads are denied by design. Call it from your backend, not client-side JS.

Authentication

Every request needs an API key in the Authorization header:

http
Authorization: Bearer sk_...

Create and revoke keys in the dashboard under API & MCP (https://inkrun.dev/api-mcp). A key is shown once at creation — store it securely.

FailureStatus
Deployment has no auth configured501
Missing / malformed / invalid key401
Key tripped its rate limit429 (+ Retry-After)

POST /api/v1/render

Render Markdown to a PDF.

Request body (JSON)

FieldTypeRequiredDescription
markdownstringyesMarkdown source (min length 1).
templatestringnoA saved template — slug or id. Supplies base theme, page format, and style tweaks.
themestringnoA base theme name (default, editorial, minimal, technical, bold) or a custom theme's slug/id. Overrides the template's theme.
formatstringnoPage size: A4, Letter, or Legal. Overrides the template's format.
titlestringnoTitle stored with the render. Defaults to the document's frontmatter title: when present.
sendEmailobjectnoDeliver the PDF by email as a download link valid for 3 days: { "to", "senderName", "message?" }. Paid plans — skipped with a notice on the free tier. See Email delivery.

Precedence when both a template and an explicit theme/format are given: the explicit values win. With no template and no theme, you get the default theme on A4.

Request headers

HeaderPurpose
Authorization: Bearer sk_...Required. Your API key.
Content-Type: application/jsonRequired for the JSON body.
Accept: application/pdfReturn the raw PDF bytes instead of JSON (see below).
Idempotency-KeyOptional retry-safety key (1–255 visible ASCII chars). See Idempotency.
x-request-idOptional correlation id; echoed back on the response (a generated one is returned if you omit it).
x-folio-sourceOptional origin tag (mcp supported); defaults to api for History/analytics.

Response — JSON (default)

200 OK with:

json
{
  "id": "rnd_9f2a…",
  "status": "rendered",
  "size": 28211,
  "url": "https://…s3…/rnd_9f2a.pdf?X-Amz-Signature=…",
  "stored": true
}
FieldDescription
idRender id (also the stored filename).
statusrendered on success.
sizePDF size in bytes.
urlPresigned download URL when S3 is configured; omitted otherwise.
storedWhether a re-downloadable copy was stored. false means storage failed — the PDF was still produced but there's no url.
storageErrorPresent only when storage failed; a short reason string.
emailPresent only when sendEmail was requested: { "sent": true, "to": "…" }. On the free tier the send is skipped: "sent": false plus "skipped": "free_tier" and a message. A runtime send failure carries an error string instead.

The x-request-id header is echoed on every response.

Response — raw PDF

Send Accept: application/pdf and you get the bytes directly:

http
200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="rnd_9f2a….pdf"

Examples

Raw PDF straight to a file:

bash
curl "https://inkrun.dev/api/v1/render" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/pdf" \
  -d '{ "template": "quarterly-report", "markdown": "# Q2 Revenue\n\nStrong quarter…" }' \
  --output report.pdf

JSON with a presigned link:

bash
curl "https://inkrun.dev/api/v1/render" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "theme": "editorial", "format": "Letter", "markdown": "# Whitepaper" }'

Idempotency

Send an Idempotency-Key header and a retried request is answered from the original render — no second render, no second quota charge:

bash
curl "https://inkrun.dev/api/v1/render" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2026-07-0042" \
  -d '{ "template": "quarterly-report", "markdown": "# Q2 Revenue" }'
  • A repeat within the retention window (24 h by default) returns the original render's { "id", "status", "size", "url", "stored" } with "replayed": true in the body and an idempotency-replayed: true response header. The url is freshly presigned on every replay.
  • Replays always return JSON (the original bytes aren't re-streamed) — use the url to download.
  • A failed original is not replayed: retrying the same key runs a fresh render (the failed attempt's quota was refunded).
  • The guard targets retries. Two simultaneous first requests with the same key may both render.

Email delivery

Pass sendEmail and Inkrun emails the recipient a branded message with a presigned download link that stays valid for 3 days (the response's url is short-lived; the emailed link is the long one). The email is attributed to senderName — “Ada sent you a PDF” — and can quote an optional personal message.

bash
curl "https://inkrun.dev/api/v1/render" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "quarterly-report",
    "markdown": "# Q2 Revenue\n\nStrong quarter…",
    "title": "Q2 Report",
    "sendEmail": {
      "to": "[email protected]",
      "senderName": "Ada Lovelace",
      "message": "Here is the report we discussed."
    }
  }'
  • Paid plans only. Email delivery requires a paid subscription. A free-tier request is not rejected: the render succeeds as usual and the response's email field reports the skip — { "sent": false, "skipped": "free_tier", "message": "…" }. Check email.sent, don't assume delivery. Full details: Email delivery → Plans.
  • Requires the deployment to have email (Resend) and S3 storage configured — otherwise the request fails with 400 before rendering. An invalid to address or empty senderName is also a 400.
  • A provider hiccup after a successful render never fails the request: you still get 200 with the PDF/link, and the outcome is reported in the response's email field ("sent": false + error). On the raw-PDF path (Accept: application/pdf) the outcome comes back in an x-email-sent response header instead.

Errors

Errors carry a stable envelope with a machine-readable code:

json
{ "error": { "code": "input_too_large", "message": "Markdown input is 5.0 MB…" } }
StatuscodeWhen
400invalid_requestMalformed body / failed validation (details are logged server-side, not returned).
400template_not_found / theme_not_foundUnknown template / theme reference.
400email_undeliverablesendEmail requested but undeliverable (bad recipient, or the deployment lacks email/S3).
413input_too_largeMarkdown over your plan's size limit — the message names the limit and your actual size.
413output_too_largeThe produced PDF exceeded the output ceiling; nothing was stored.
429quota_exceededMonthly render quota exhausted.
429rate_limitedBurst rate limit tripped (+ Retry-After).
503pool_busyRender pool saturated (+ Retry-After: 5).
504render_timeoutThe render exceeded the per-render time budget (45 s by default). Simplify or split the document.
500internal_errorUnexpected failure — quote the x-request-id when contacting support.

Auth failures (401 missing/invalid key, 501 API not configured) still use the simple { "error": "…" } string shape.

On 429 / 503, honour Retry-After (seconds) and retry. 4xx errors are not retryable as-is; 504 may succeed with a smaller/simpler document.

Reading renders back

The id returned by a render POST stays queryable, so you can poll status, re-fetch metadata, and mint fresh download links long after the original response's presigned url expired.

GET /api/v1/render/{id}

bash
curl "https://inkrun.dev/api/v1/render/pdf_9f2a4e" \
  -H "Authorization: Bearer $INKRUN_API_KEY"
json
{
  "id": "pdf_9f2a4e",
  "status": "rendered",
  "size": 421888,
  "source": "api",
  "format": "A4",
  "title": "Q2 Report",
  "createdAt": "2026-07-07T10:12:00.000Z",
  "completedAt": "2026-07-07T10:12:03.000Z",
  "stored": true,
  "url": "https://…s3…?X-Amz-Signature=…"
}
  • url is a fresh, short-lived presigned link, minted on every call — fetch this endpoint again whenever you need to re-download.
  • stored: false means no re-downloadable copy exists (url omitted).
  • Failed renders include an errorMessage string.
  • A render that doesn't exist or belongs to another account returns 404 not_found — ids are never confirmed across accounts.

GET /api/v1/renders

Your render history, newest first, cursor-paginated:

bash
curl "https://inkrun.dev/api/v1/renders?limit=20&status=rendered" \
  -H "Authorization: Bearer $INKRUN_API_KEY"
json
{
  "renders": [ { "id": "pdf_9f2a4e", "status": "rendered", "size": 421888, "…": "…" } ],
  "next_cursor": "eyJ0IjoiMjAyNi0wNy0wN1QxMDoxMjowMC4wMDBaIiwiaWQiOiJwZGZfOWYyYTRlIn0"
}
Query paramDefaultNotes
limit201–100.
cursorOpaque cursor from the previous page's next_cursor.
sourceallFilter: web | api | mcp.
statusallFilter: queued | processing | rendered | failed.

next_cursor is null on the last page. Paging is stable while new renders arrive (keyset, not offset). List items carry metadata only — get a download url from GET /api/v1/render/{id}.

Templates API

Manage the style options you render with. All endpoints use the same Authorization: Bearer sk_... auth.

GET /api/v1/templates

List your account templates plus the built-in system templates.

bash
curl "https://inkrun.dev/api/v1/templates" \
  -H "Authorization: Bearer $INKRUN_API_KEY"
json
{
  "templates": [
    {
      "id": "tpl_…", "name": "Sales Proposal", "slug": "sales-proposal",
      "description": "…", "themeName": "default", "format": "A4",
      "styles": { "accent": "#1F6FEB", "fontHeading": "sans", "fontBody": "sans" },
      "system": true, "createdAt": "2026-01-05T09:01:00.000Z"
    }
  ],
  "next_cursor": null
}

Optionally paginated: ?limit= (1–100, default 100) + ?cursor= (opaque, from the previous page's next_cursor; null on the last page). Omitting both returns the full list for typical accounts — existing clients keep working.

POST /api/v1/templates

Create a template owned by your account. Returns 201 with the created record.

FieldTypeNotes
namestringRequired. 1–80 chars.
descriptionstringOptional, ≤280 chars.
themeNamestringBase theme: a built-in name or a custom theme's slug. Default default.
formatstringA4 | Letter | Legal. Default A4.
styles.accentstringHex colour, e.g. #2f5d50.
styles.fontBody / fontHeading / fontMonostringFont preset key (see Templates → Fonts).
bash
curl "https://inkrun.dev/api/v1/templates" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Proposal",
    "themeName": "default",
    "format": "A4",
    "styles": { "accent": "#1F6FEB", "fontHeading": "sans", "fontBody": "serif" }
  }'

Unknown/invalid style fields are dropped (only hex accents and known font presets survive). Then render with the returned slug:

bash
curl "https://inkrun.dev/api/v1/render" \
  -H "Authorization: Bearer $INKRUN_API_KEY" \
  -H "Content-Type: application/json" -H "Accept: application/pdf" \
  -d '{ "template": "acme-proposal", "markdown": "# Proposal for Acme" }' \
  --output proposal.pdf

PATCH /api/v1/templates/{slugOrId}

Partial update — only the fields you send change; the slug is preserved so existing render references keep working. Same field shapes as POST. 404 if no matching template you own.

DELETE /api/v1/templates/{slugOrId}

Soft-delete (archive) one of your templates. 404 if not found.

json
{ "slug": "acme-proposal", "deleted": true }
Built-in system templates are read-only — you can render with them but not edit or delete them.

Quotas, rate limits & size limits

Independent limits, all scaling with your plan:

PlanMonthly renders (quota)API key rate limitMax Markdown input
Free501 request / sec1 MiB
Pro5005 requests / sec5 MiB
  • Quota is per calendar month (UTC). Exceeding it → 429 quota_exceeded. Failed renders don't consume quota (they're refunded).
  • Rate limit is a per-key burst guard. Tripping it → 429 rate_limited with a Retry-After header; back off and retry.
  • Input size is measured in UTF-8 bytes of the markdown field. Over the cap → 413 input_too_large naming the limit and your actual size.
  • Output size: a produced PDF over 50 MiB fails with 413 output_too_large (quota refunded) instead of being delivered.
  • Render time: a render has a 45 s budget; exceeding it → 504 render_timeout. Split very large or complex documents.

Versioning

/api/v1 is the stable version: changes are additive only — new endpoints and new optional/response fields; ignore fields you don't recognise. Breaking changes ship as a new version path, announced with Deprecation/Sunset headers and a 6-month overlap.

Charts

Any render (REST, MCP, or dashboard) can include charts via a ```chart fenced code block. Full spec and a worked example: Templates → Charts.

Inkrun © 2026