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

# Render PDF

> Render PDFs over HTTP with the self-hosted server — sync rendering, form flattening, and embedded-data extraction.

These endpoints are served by the **self-hosted server** — the same engine behind `npm install`, packaged as a Docker image:

```bash theme={null}
docker run -p 3000:3000 formepdf/forme
```

Base URL below is `http://localhost:3000` (substitute your deployment). Authentication is optional: set the `FORME_API_KEY` environment variable on the container and pass it as a Bearer token; with no key set, endpoints are open. See [Self-Hosting](/self-hosting) for full setup.

> The hosted API at `api.formepdf.com` has been retired — Forme is open-source only. If you'd rather not run a server at all, every operation here is also a function call in [@formepdf/core](/typescript-sdk).

***

## Render PDF (sync)

`POST /v1/render/:slug`

Renders a PDF from a template and returns the file directly.

**Path params:** `slug` — your template's URL slug.

**Body:** JSON object passed as template data. All fields are forwarded to your JSX template function.

**Optional fields:**

* `s3` — upload the PDF to your S3 bucket instead of returning bytes (see [S3 Upload](#s3-upload) below)
* `save` — `boolean`, default `true`. Every render is auto-saved to your [Documents](/concepts/documents) with `source: "generated"`. Set `false` to skip saving.
* `saveName` — `string`, optional custom document name. Default: `{slug}-{YYYY-MM-DD}` (e.g. `invoice-2026-04-03`).
* `metadata` — `object`, optional developer-defined key-value pairs stored on the saved document. Useful for tagging renders with your own identifiers (customer ID, department, environment, etc.). See [Metadata](#metadata) below.

<CodeGroup>
  ```bash curl theme={null}
  curl http://localhost:3000/v1/render/invoice \
    -H "Authorization: Bearer forme_sk_abc123..." \
    -H "Content-Type: application/json" \
    -d '{"clientName": "Jane Smith", "date": "2024-01-15", "dueDate": "2024-02-15", "items": [{"description": "Consulting", "quantity": 10, "unitPrice": 150}]}' \
    --output invoice.pdf
  ```

  ```javascript Node.js theme={null}
  const res = await fetch("http://localhost:3000/v1/render/invoice", {
    method: "POST",
    headers: {
      Authorization: "Bearer forme_sk_abc123...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      clientName: "Jane Smith",
      date: "2024-01-15",
      dueDate: "2024-02-15",
      items: [{ description: "Consulting", quantity: 10, unitPrice: 150 }],
    }),
  });

  const pdf = Buffer.from(await res.arrayBuffer());
  fs.writeFileSync("invoice.pdf", pdf);
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "http://localhost:3000/v1/render/invoice",
      headers={
          "Authorization": "Bearer forme_sk_abc123...",
          "Content-Type": "application/json",
      },
      json={
          "clientName": "Jane Smith",
          "date": "2024-01-15",
          "dueDate": "2024-02-15",
          "items": [{"description": "Consulting", "quantity": 10, "unitPrice": 150}],
      },
  )

  with open("invoice.pdf", "wb") as f:
      f.write(res.content)
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	body, _ := json.Marshal(map[string]any{
  		"clientName": "Jane Smith",
  		"date":       "2024-01-15",
  		"dueDate":    "2024-02-15",
  		"items": []map[string]any{
  			{"description": "Consulting", "quantity": 10, "unitPrice": 150},
  		},
  	})

  	req, _ := http.NewRequest("POST", "http://localhost:3000/v1/render/invoice", bytes.NewReader(body))
  	req.Header.Set("Authorization", "Bearer forme_sk_abc123...")
  	req.Header.Set("Content-Type", "application/json")

  	res, _ := http.DefaultClient.Do(req)
  	defer res.Body.Close()

  	out, _ := os.Create("invoice.pdf")
  	io.Copy(out, res.Body)
  }
  ```
</CodeGroup>

**Response:** `200 OK` with `Content-Type: application/pdf` body.

***

## Extract Embedded Data

`POST /v1/extract`

Extract embedded JSON data from a PDF that was rendered with `embedData`.

Send the raw PDF bytes as the request body with `Content-Type: application/pdf`.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST http://localhost:3000/v1/extract \
    -H "Authorization: Bearer forme_sk_abc123..." \
    -H "Content-Type: application/pdf" \
    --data-binary @invoice.pdf
  ```

  ```javascript Node.js theme={null}
  const pdf = fs.readFileSync("invoice.pdf");
  const res = await fetch("http://localhost:3000/v1/extract", {
    method: "POST",
    headers: {
      Authorization: "Bearer forme_sk_abc123...",
      "Content-Type": "application/pdf",
    },
    body: pdf,
  });

  const { data } = await res.json();
  console.log(data); // the original JSON passed to embedData
  ```

  ```python Python theme={null}
  with open("invoice.pdf", "rb") as f:
      pdf_bytes = f.read()

  res = requests.post(
      "http://localhost:3000/v1/extract",
      headers={
          "Authorization": "Bearer forme_sk_abc123...",
          "Content-Type": "application/pdf",
      },
      data=pdf_bytes,
  )

  print(res.json()["data"])
  ```
</CodeGroup>

**Response:** `200 OK`

```json theme={null}
{ "data": { "clientName": "Jane Smith", "items": [...] } }
```

Returns `404` if no embedded data is found.

***

## Flatten Forms

`POST /v1/render/:slug?flattenForms=true`

Render a PDF with all form fields flattened — interactive fields are converted to static content. Useful for filling a form template with data and sending a non-editable PDF.

Pass `flattenForms=true` as a query parameter on any render endpoint (sync or async).

***

## Error Format

All errors return JSON:

```json theme={null}
{ "error": "Human-readable error message" }
```

Common HTTP status codes:

* `400` — Invalid request (missing fields, bad S3 config)
* `401` — Missing or invalid API key
* `404` — Template or resource not found
* `429` — Rate limit or usage limit exceeded
* `502` — S3 upload failed
* `500` — Internal server error
