<!-- SKILL-VERSION: pdf-integration 3ed568e@2026-06-05T18:19:05+10:00 -->
> **Skill version:** `3ed568e@2026-06-05T18:19:05+10:00` (`pdf-integration`).
> If you have a cached copy of this skill with a different version token,
> discard it and use this copy. Re-fetch this URL at the start of each
> session to detect updates.

# makesPDF — application integration

Companion to [`pdf-template-author.md`](./pdf-template-author.md). This file
is for **wiring makesPDF into a server-side codebase** — once a template
exists, how do you call the API from your app to render PDFs with real
data?

If you're authoring a template, read `pdf-template-author.md` instead. If
you're an AI agent that needs a Bearer token to act on a user's behalf,
read [`pdf-auth.md`](./pdf-auth.md) — that's the OAuth device flow, which
is **not** what backend integration uses.

## TL;DR for the integrating developer

1. Sign in to [makespdf.com](https://makespdf.com), go to
   `/settings/api-keys`, create a key, and store it as `MAKESPDF_API_KEY`
   in your server environment (one key per environment).
2. Save your template once via `POST /api/v1/templates` and persist the
   returned `templateId` as a config value.
3. From your app, `POST /api/v1/render` with `{ templateId, data }` and a
   `Authorization: Bearer $MAKESPDF_API_KEY` header. The response body is
   the PDF bytes.
4. Surface the PDF however your product needs it — stream to the browser,
   save to object storage, attach to an email, queue for batch.

The rest of this file fills in the details: which endpoint to pick, error
handling, snippets for the languages most apps are written in, and the
patterns that come up when you put this in production.

---

## Choose your endpoint

Pick by **what your app already has**, not by what sounds most powerful:

| You have…                                       | Call                                                          | Notes                                                           |
| ----------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------- |
| A `templateId` and a `data` object              | `POST /api/v1/render`                                         | The common case. Billed (1 credit / 10 pages).                  |
| A markdown string                               | `POST /api/v1/md`                                             | Free of template-authoring cost; default styling.               |
| Inline DSL you're still iterating on            | `POST /api/v1/preview`                                        | Free; outputs draft watermark + filler text. Don't use in prod. |
| Either of the above and want a pre-flight check | `POST /api/v1/preview/validate` or `POST /api/v1/md/validate` | Cheap, no rendering. Good for surfacing issues to authors.      |

If you have markdown today and a templated layout next quarter, the
integration shape is the same — just change which endpoint you hit. Auth,
error handling, and response handling are identical across all four.

---

## Authentication

`/api/v1/*` requires a Bearer token by default. For server-side
integration, that token is an **API key** you mint from your makesPDF
account.

> **Note for low-volume Markdown callers.** `POST /api/v1/md` has an
> anonymous carve-out for low-volume usage (e.g. quick scripts, IDE
> plugins). When the carve-out is enabled, requests without an
> Authorization header succeed under stricter per-IP rate limits
> (60/hour, 200/day, max 20 pages per render). For any production
> integration — higher limits, artifact capture, the other endpoints —
> use an API key.

1. Sign in at [makespdf.com](https://makespdf.com).
2. Visit [`/settings/api-keys`](https://makespdf.com/settings/api-keys).
3. Create a key. Copy it once — it isn't shown again.
4. Store it as an environment variable in your deployment (e.g.
   `MAKESPDF_API_KEY`). Treat it like any other production secret: never
   commit it, mint a separate key per environment (dev / staging / prod),
   and rotate on staff turnover.

On every request:

```
Authorization: Bearer ${MAKESPDF_API_KEY}
```

> **Not the same as the OAuth device flow.** The flow described in
> [`pdf-auth.md`](./pdf-auth.md) is for an AI agent or CLI acting on
> behalf of a logged-in user. Backend integrations don't have a
> human-in-the-loop on every render — use a long-lived API key instead.

---

## Saving your template

Templates created in the makesPDF builder live in your account and have
a stable UUID. Treat that UUID like a config value, not a database record.

```bash
# One-time setup: save the DSL and capture the templateId
jq -n --rawfile dsl invoice.template.js --arg name "Invoice v1" \
  '{ dsl: $dsl, name: $name }' |
  curl -sX POST https://makespdf.com/api/v1/templates \
    -H "Authorization: Bearer $MAKESPDF_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @-
# → { "templateId": "11111111-2222-…", "name": "Invoice v1", "createdAt": … }
```

Where to put the resulting id depends on your stack:

- **Single template app** — `MAKESPDF_INVOICE_TEMPLATE_ID=…` in your env.
- **A handful** — a typed constant map next to your config:
  ```ts
  export const TEMPLATES = {
    invoice: process.env.MAKESPDF_INVOICE_ID!,
    receipt: process.env.MAKESPDF_RECEIPT_ID!,
  } as const;
  ```
- **Many / per-tenant** — store on the row that triggers the render
  (`organisations.invoice_template_id`, etc.) so each tenant can swap
  their own template without a deploy.

You can update a template in place via `PUT /api/v1/templates/:id`; the
id stays the same, so existing callers keep working.

---

## The render call

Universal request shape — every language snippet below boils down to
this:

```
POST /api/v1/render
Authorization: Bearer $MAKESPDF_API_KEY
Content-Type: application/json
Accept: application/pdf            # optional; the default

{
  "templateId": "11111111-2222-…",
  "data": { "invoiceNumber": "INV-042", "items": [ … ] },
  "options": { "title": "Invoice INV-042" }   // optional
}
```

Response: PDF bytes (`Content-Type: application/pdf`). Useful headers:

- `X-Pages` — pages rendered.
- `X-Credits-Deducted` — credits used (1 per 10 pages).
- `X-Credits-Remaining` — your account's remaining balance after this
  request. Watch this in logs to spot trending-toward-empty before
  customers see a 402.

If you'd rather get JSON metadata than the binary (for logging, async
flows, or returning a presigned URL to the browser), send
`Accept: application/json` and you'll get
`{ url, pages, credits_deducted, remaining_balance }` back instead.

---

## Errors

Every error path returns a JSON body. You only need to handle a small
number of cases:

| Status | Meaning                                                        | Retry? | What to do                                                     |
| ------ | -------------------------------------------------------------- | ------ | -------------------------------------------------------------- |
| 400    | Malformed JSON, missing/invalid `templateId`, bad `data` shape | No     | Fix the payload. Surface to whoever wrote the calling code.    |
| 401    | Bad / missing Bearer token                                     | No     | Check your env var is loaded. Rotate if compromised.           |
| 402    | Account out of credits                                         | No     | Alert ops. Top up at `/settings/billing` or upgrade plan.      |
| 404    | Unknown `templateId` **or** not owned by this key              | No     | Wiring error — check the id matches the environment's account. |
| 429    | Rate limit (200 renders / hour)                                | Yes    | Back off, respect `Retry-After` if present.                    |
| 5xx    | Transient server error                                         | Yes    | Exponential backoff, ≤3 attempts.                              |

A simple, correct retry policy:

```ts
async function renderWithRetry(payload: unknown, attempt = 0): Promise<Response> {
  const res = await fetch("https://makespdf.com/api/v1/render", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAKESPDF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
  if (res.ok) return res;
  if ((res.status === 429 || res.status >= 500) && attempt < 3) {
    const retryAfter = Number(res.headers.get("retry-after")) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return renderWithRetry(payload, attempt + 1);
  }
  throw new Error(`makesPDF render failed: ${res.status} ${await res.text()}`);
}
```

Don't blanket-retry on 4xx — those won't change on the next call and
you'll burn rate limit for nothing.

---

## Code samples

Every snippet below: read the API key from env, POST the render, throw
on non-2xx, return the bytes. Adapt the storage / response step to your
framework.

### Node / TypeScript (works in Node 18+, Cloudflare Workers, Vercel Edge, Deno, Bun)

```ts
export async function renderInvoice(data: unknown): Promise<Uint8Array> {
  const res = await fetch("https://makespdf.com/api/v1/render", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAKESPDF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      templateId: process.env.MAKESPDF_INVOICE_TEMPLATE_ID,
      data,
    }),
  });
  if (!res.ok) {
    throw new Error(`makesPDF ${res.status}: ${await res.text()}`);
  }
  return new Uint8Array(await res.arrayBuffer());
}
```

### Next.js route handler (streams the PDF straight to the browser)

```ts
// app/api/invoice/[id]/route.ts
export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const data = await loadInvoice(params.id);
  const upstream = await fetch("https://makespdf.com/api/v1/render", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAKESPDF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      templateId: process.env.MAKESPDF_INVOICE_TEMPLATE_ID,
      data,
    }),
  });
  if (!upstream.ok) {
    return new Response(await upstream.text(), { status: upstream.status });
  }
  return new Response(upstream.body, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${params.id}.pdf"`,
    },
  });
}
```

### Python (httpx)

```python
import os, httpx

def render_invoice(data: dict) -> bytes:
    res = httpx.post(
        "https://makespdf.com/api/v1/render",
        headers={"Authorization": f"Bearer {os.environ['MAKESPDF_API_KEY']}"},
        json={"templateId": os.environ["MAKESPDF_INVOICE_TEMPLATE_ID"], "data": data},
        timeout=30,
    )
    res.raise_for_status()
    return res.content
```

### Go (net/http)

```go
func RenderInvoice(ctx context.Context, data any) ([]byte, error) {
    body, _ := json.Marshal(map[string]any{
        "templateId": os.Getenv("MAKESPDF_INVOICE_TEMPLATE_ID"),
        "data":       data,
    })
    req, _ := http.NewRequestWithContext(ctx, "POST",
        "https://makespdf.com/api/v1/render", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("MAKESPDF_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    if res.StatusCode >= 300 {
        msg, _ := io.ReadAll(res.Body)
        return nil, fmt.Errorf("makesPDF %d: %s", res.StatusCode, msg)
    }
    return io.ReadAll(res.Body)
}
```

### Ruby (Net::HTTP)

```ruby
require "net/http"
require "json"

def render_invoice(data)
  uri = URI("https://makespdf.com/api/v1/render")
  req = Net::HTTP::Post.new(uri, {
    "Authorization" => "Bearer #{ENV.fetch('MAKESPDF_API_KEY')}",
    "Content-Type"  => "application/json",
  })
  req.body = { templateId: ENV.fetch("MAKESPDF_INVOICE_TEMPLATE_ID"), data: data }.to_json
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
  raise "makesPDF #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
  res.body
end
```

### PHP (curl)

```php
function render_invoice(array $data): string {
    $ch = curl_init("https://makespdf.com/api/v1/render");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer " . getenv("MAKESPDF_API_KEY"),
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode([
            "templateId" => getenv("MAKESPDF_INVOICE_TEMPLATE_ID"),
            "data" => $data,
        ]),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($code >= 300) throw new RuntimeException("makesPDF $code: $body");
    return $body;
}
```

### curl (for ops sanity checks)

```bash
curl -fsS -X POST https://makespdf.com/api/v1/render \
  -H "Authorization: Bearer $MAKESPDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"templateId\":\"$MAKESPDF_INVOICE_TEMPLATE_ID\",\"data\":$(cat data.json)}" \
  -o invoice.pdf
```

---

## Delivering the PDF

The render call gives you bytes. What you do with them depends on the
shape of your product:

- **Stream straight to the browser** — set `Content-Type: application/pdf`
  and `Content-Disposition: attachment; filename="…"`. The Next.js
  example above does this. Browsers will preview inline if you use
  `inline` instead of `attachment`.
- **Save to object storage (S3, R2, GCS)** — upload the bytes, store the
  resulting URL on the record that triggered the render. Best when the
  PDF will be downloaded multiple times or referenced by URL (email
  links, customer portal, etc.).
- **Email attachment** — most email APIs (Mailgun, SendGrid, SES,
  Postmark) take a base64-encoded body for attachments. Render, base64,
  attach, send.
- **Background job for big batches** — if you're rendering hundreds of
  PDFs (monthly statements, end-of-day invoices), enqueue one job per
  document and have a worker call `/render`, store the result, and
  notify on completion. Don't render a thousand PDFs synchronously inside
  a single web request — you'll trip the 200/hour rate limit and your
  request will time out.

---

## Performance & limits

- **Latency** — typical renders complete in well under a second. A
  20-page invoice is around 200–400 ms; a 100-page report is around 1–2
  seconds.
- **Rate limit** — 200 render calls per hour per API key. Higher
  throughput on request — contact support.
- **Payload size** — `data` payloads aren't capped explicitly, but the
  worker has a 128 MB memory ceiling and a 30-second CPU budget. In
  practice, anything under a few MB of JSON is fine; if you're shipping
  more than that, you're probably uploading images that should be hosted
  separately and referenced by URL.
- **Caching** — if your `data` is stable (e.g. a published invoice
  doesn't change), hash the canonical request body and cache the
  resulting PDF in your storage layer. Subsequent requests for the same
  invoice skip `/render` entirely. This is a meaningful saving — both in
  credits and in latency.

---

## Testing

A few patterns that keep tests fast and your bill small:

- **Unit tests** — mock the HTTP client. Don't hit the live API. The
  request shape and headers are stable; assert against those.
- **Integration tests** — call `/api/v1/preview` with the inline DSL
  instead of `/render`. `/preview` is free and produces deterministic
  output (filler text, draft overlay) — great for "the request shape and
  template parse cleanly" checks without burning credits.
- **Snapshot tests** — don't snapshot raw PDF bytes. The PDF format
  embeds timestamps and object IDs that vary between renders. Snapshot
  the request body, or extract text via `pdfjs` and snapshot that.
- **Local dev** — set `MAKESPDF_DEV_TOKEN` in your dev env and use it
  as the Bearer token to skip OAuth during local iteration. Production
  rejects this token automatically.

---

## Versioning a template

Templates are mutable: `PUT /api/v1/templates/:id` updates the DSL in
place, the id stays the same, and the next `/render` call picks up the
new version. That's fine for forward-compatible tweaks (style changes,
new optional fields).

For breaking changes — renaming a required `data` key, dropping a
section the old data still references — don't mutate. Save a **second**
template (`Invoice v2`), put a feature flag in your code that picks the
new id, and migrate callers gradually:

```ts
const templateId = features.invoiceV2(orgId) ? TEMPLATES.invoiceV2 : TEMPLATES.invoice;
```

When traffic on the old id hits zero, delete it via `DELETE /api/v1/templates/:id`.

---

## CLI vs API

`@makespdf/cli` is a thin wrapper around the same endpoints documented
here. Use it for ops scripts and one-offs (`makespdf md report.md -o
report.pdf`) — but for application code, call the HTTP API directly.
The CLI doesn't add anything you can't do with `fetch`, and shelling out
of a web server adds a process boundary you don't want on a hot path.

---

## Pointers

- **API reference** — [`/skills/pdf-api.md`](https://makespdf.com/skills/pdf-api.md).
  Endpoint-by-endpoint reference for every `/api/v1/*` route, including
  ones not covered here (`/templates`, `/library`, validation
  endpoints).
- **Template authoring** — [`/skills/pdf-template-author.md`](https://makespdf.com/skills/pdf-template-author.md).
  How to write the DSL in the first place. Read this before you reach
  for `/render`; the template has to exist.
- **Custom fonts** — [`/skills/pdf-fonts.md`](https://makespdf.com/skills/pdf-fonts.md).
  Per-request and account-level font registration.
- **OAuth device flow** — [`/skills/pdf-auth.md`](https://makespdf.com/skills/pdf-auth.md).
  Only relevant if you're an AI agent or CLI acting on behalf of a user
  — not for backend integration.
- **Docs index** — [`/docs`](https://makespdf.com/docs).
- **CLI** — [`@makespdf/cli`](https://github.com/makesPDF/makespdf-cli).
