> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tipar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Generate your first PDF in a few minutes — create a key, make one call, get a document back.

This walks you from zero to a PDF. You'll create an API key, make a single `POST` request, and get a document back in the response.

## 1. Create an API key

<Steps>
  <Step title="Sign in to the dashboard">
    Go to [app.tipar.dev](https://app.tipar.dev) and sign in with your email. Tipar is passwordless — we send you a magic link, no password to set or forget.
  </Step>

  <Step title="Mint a key">
    Open **API keys** and create one. The full key is shown **once**, at creation — copy it now and store it somewhere safe (a secrets manager, your platform's env vars). Tipar only keeps a hash, so a lost key can't be recovered, only revoked and replaced.
  </Step>
</Steps>

Keys look like `tipar_live_` followed by 32 characters. You send the key as a bearer token on every request to `/generate`. Full details in [Authentication](/authentication).

<Warning>
  Treat the key like a password. It belongs in an environment variable on your server, never in client-side code, a mobile app, or a public repository.
</Warning>

## 2. Generate your first PDF

The smallest possible request: a one-line template and a single data field. Replace `tipar_live_your_key` with your real key.

<CodeGroup>
  ```bash curl theme={"dark"}
  curl https://api.tipar.dev/generate \
    -H "Authorization: Bearer tipar_live_your_key" \
    -H "Content-Type: application/json" \
    -d '{
      "template": { "page": { "content": { "type": "text", "value": "Hello, {{name}}!", "style": { "fontSize": 24, "bold": true } } } },
      "data": { "name": "world" }
    }' \
    --output hello.pdf
  ```

  ```javascript Node.js theme={"dark"}
  import { writeFile } from "node:fs/promises";

  const res = await fetch("https://api.tipar.dev/generate", {
    method: "POST",
    headers: {
      Authorization: "Bearer tipar_live_your_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      template: { page: { content: { type: "text", value: "Hello, {{name}}!", style: { fontSize: 24, bold: true } } } },
      data: { name: "world" },
    }),
  });

  if (!res.ok) throw new Error(`Tipar ${res.status}: ${await res.text()}`);
  await writeFile("hello.pdf", Buffer.from(await res.arrayBuffer()));
  ```

  ```python Python theme={"dark"}
  import requests

  res = requests.post(
      "https://api.tipar.dev/generate",
      headers={"Authorization": "Bearer tipar_live_your_key"},
      json={
          "template": {"page": {"content": {"type": "text", "value": "Hello, {{name}}!", "style": {"fontSize": 24, "bold": True}}}},
          "data": {"name": "world"},
      },
  )
  res.raise_for_status()
  with open("hello.pdf", "wb") as f:
      f.write(res.content)
  ```

  ```php PHP theme={"dark"}
  <?php
  $ch = curl_init("https://api.tipar.dev/generate");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: Bearer tipar_live_your_key",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "template" => ["page" => ["content" => ["type" => "text", "value" => "Hello, {{name}}!", "style" => ["fontSize" => 24, "bold" => true]]]],
          "data" => ["name" => "world"],
      ]),
  ]);
  file_put_contents("hello.pdf", curl_exec($ch));
  ```

  ```ruby Ruby theme={"dark"}
  require "net/http"
  require "json"

  uri = URI("https://api.tipar.dev/generate")
  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer tipar_live_your_key"
  req["Content-Type"] = "application/json"
  req.body = {
    template: { page: { content: { type: "text", value: "Hello, {{name}}!", style: { fontSize: 24, bold: true } } } },
    data: { name: "world" },
  }.to_json

  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
  File.binwrite("hello.pdf", res.body)
  ```
</CodeGroup>

You now have `hello.pdf` on disk. That's the whole loop: one request in, one PDF out.

<Note>
  A successful response is `200 OK` with `Content-Type: application/pdf` and the raw PDF bytes as the body. There is no JSON wrapper and no job to poll — `/generate` is synchronous.
</Note>

## 3. Render a real document

A single line of text isn't why you're here. Here's a complete one-page invoice — a header, a line-item table that repeats over your data, and a footer. Send this exactly as-is to see it render.

<CodeGroup>
  ```json template theme={"dark"}
  {
    "page": {
      "size": "A4",
      "margin": 40,
      "defaultTextStyle": { "fontSize": 10 },
      "header": {
        "type": "row",
        "items": [
          { "size": { "kind": "relative", "weight": 1 }, "child": { "type": "text", "value": "{{company.name}}", "style": { "fontSize": 18, "bold": true } } },
          { "size": { "kind": "constant", "width": 180 }, "child": { "type": "column", "items": [
            { "type": "text", "value": "INVOICE", "style": { "fontSize": 16, "bold": true }, "align": "right" },
            { "type": "text", "value": "{{invoice.number}} · {{invoice.date}}", "style": { "fontSize": 9, "color": "grey.darken1" }, "align": "right" }
          ] } }
        ]
      },
      "content": {
        "type": "column",
        "spacing": 14,
        "items": [
          { "type": "column", "items": [
            { "type": "text", "value": "Bill to", "style": { "fontSize": 9, "color": "grey.darken1" } },
            { "type": "text", "value": "{{customer.name}}", "style": { "bold": true } },
            { "type": "text", "value": "{{customer.address}}" }
          ] },
          {
            "type": "table",
            "columns": [
              { "kind": "relative", "weight": 5 },
              { "kind": "relative", "weight": 1 },
              { "kind": "relative", "weight": 2 }
            ],
            "header": { "row": [
              { "child": { "type": "text", "value": "Description", "style": { "semiBold": true, "fontSize": 9 } }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 5 },
              { "child": { "type": "text", "value": "Qty", "style": { "semiBold": true, "fontSize": 9 }, "align": "right" }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 5 },
              { "child": { "type": "text", "value": "Amount", "style": { "semiBold": true, "fontSize": 9 }, "align": "right" }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 5 }
            ] },
            "body": {
              "forEach": "invoice.lines",
              "row": [
                { "child": { "type": "text", "value": "{{item.description}}" }, "border": { "bottom": 1, "color": "grey.lighten2" }, "padding": 5 },
                { "child": { "type": "text", "value": "{{item.qty}}", "align": "right" }, "border": { "bottom": 1, "color": "grey.lighten2" }, "padding": 5 },
                { "child": { "type": "text", "value": "{{item.amount}}", "align": "right" }, "border": { "bottom": 1, "color": "grey.lighten2" }, "padding": 5 }
              ]
            }
          },
          { "type": "text", "value": "Total due: {{invoice.total}}", "style": { "bold": true, "fontSize": 13 }, "align": "right" }
        ]
      },
      "footer": {
        "type": "text",
        "value": "{{company.name}} · {{company.email}} · Payable within 14 days",
        "style": { "fontSize": 8, "color": "grey.darken1" },
        "align": "center"
      }
    }
  }
  ```

  ```json data theme={"dark"}
  {
    "company": { "name": "Acme Analytics", "email": "billing@acme-analytics.io" },
    "customer": { "name": "Globex GmbH", "address": "Friedrichstr. 68, 10117 Berlin, Germany" },
    "invoice": {
      "number": "INV-2047",
      "date": "2026-06-01",
      "total": "€87.00",
      "lines": [
        { "description": "Pro plan — June 2026", "qty": 1, "amount": "€49.00" },
        { "description": "Additional seats", "qty": 3, "amount": "€30.00" },
        { "description": "SMS credits top-up", "qty": 1, "amount": "€8.00" }
      ]
    }
  }
  ```
</CodeGroup>

Notice the moving parts:

* `{{company.name}}`, `{{invoice.total}}` — [interpolation](/templates/overview#interpolation) tokens, replaced with values from `data`.
* The `table` body uses `forEach: "invoice.lines"` to render **one row per line item**. Inside those rows, `{{item.description}}` refers to the current line. See [Tables](/templates/tables).
* Totals are pre-formatted strings (`"€87.00"`). Tipar does no math — you compute and format values, Tipar lays them out. See [why there are no expressions](/templates/overview#two-rules).

## Next steps

<CardGroup cols={2}>
  <Card title="Learn the template language" icon="layer-group" href="/templates/overview">
    Layout, text, tables, images, headers and footers.
  </Card>

  <Card title="Worked examples" icon="copy" href="/examples/invoice">
    Complete, copy-pasteable invoice, receipt, certificate and report templates.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/generate">
    Every parameter, status code and error of `POST /generate`.
  </Card>

  <Card title="Handle errors" icon="triangle-exclamation" href="/api-reference/errors">
    What 400, 401, 402, 422 and 429 mean and how to react.
  </Card>
</CardGroup>
