---
title: "Make a document"
method: POST
path: "/api/render"
tags: ["Documents"]
---

# Make a document

`POST /api/render`

## Request

- Base URL: `https://morfee.furo.solutions`
- URL: `https://morfee.furo.solutions/api/render`
- Auth: one of:
  - API key in header `X-Api-Key`
  - HTTP bearer

The real work. A template plus data comes back as a PDF, as a download link, or as the rendered HTML. This is the only route in the product that costs a render.

Needs a key scoped `render` or `full`.

## Query parameters

- `format` 'pdf' | 'url' | 'html'
- `templateId` string

## Headers

- `Idempotency-Key` string

## Request body

- union: A document comes about in exactly one of three ways, and everything after that is the same: the same key, the same quota, the same queue, the same formats. Two of the three in one request is a 400 naming both, because a caller who sends both believes something about this call that is not true.
  - union
    - object
      - `templateId` string, required: A stored template. `template` is accepted as the same field, and so is ?templateId= in the query.
      - `template` string: An older name for templateId. Same thing.
      - `hbs` string: A template that is not stored, rendered with `data`. Held to the same helpers as a stored one, so an invented helper is a 400 that names it.
      - `html` string: Finished HTML. No Handlebars step and no data: what you send is what gets printed.
      - `data` unknown
      - `filename` string: The name in the download header. `.pdf` is added if missing.
      - `format` 'pdf' | 'url' | 'html': May also go in the query string, which wins over this field.
    - object
      - `templateId` string: A stored template. `template` is accepted as the same field, and so is ?templateId= in the query.
      - `template` string, required: An older name for templateId. Same thing.
      - `hbs` string: A template that is not stored, rendered with `data`. Held to the same helpers as a stored one, so an invented helper is a 400 that names it.
      - `html` string: Finished HTML. No Handlebars step and no data: what you send is what gets printed.
      - `data` unknown
      - `filename` string: The name in the download header. `.pdf` is added if missing.
      - `format` 'pdf' | 'url' | 'html': May also go in the query string, which wins over this field.
  - object
    - `templateId` string: A stored template. `template` is accepted as the same field, and so is ?templateId= in the query.
    - `template` string: An older name for templateId. Same thing.
    - `hbs` string, required: A template that is not stored, rendered with `data`. Held to the same helpers as a stored one, so an invented helper is a 400 that names it.
    - `html` string: Finished HTML. No Handlebars step and no data: what you send is what gets printed.
    - `data` unknown
    - `filename` string: The name in the download header. `.pdf` is added if missing.
    - `format` 'pdf' | 'url' | 'html': May also go in the query string, which wins over this field.
  - object
    - `templateId` string: A stored template. `template` is accepted as the same field, and so is ?templateId= in the query.
    - `template` string: An older name for templateId. Same thing.
    - `hbs` string: A template that is not stored, rendered with `data`. Held to the same helpers as a stored one, so an invented helper is a 400 that names it.
    - `html` string, required: Finished HTML. No Handlebars step and no data: what you send is what gets printed.
    - `data` unknown
    - `filename` string: The name in the download header. `.pdf` is added if missing.
    - `format` 'pdf' | 'url' | 'html': May also go in the query string, which wins over this field.

## Response `200`

The document. Which of the three you get is decided by `format`, and it is the one place in this API where the same call answers three content types.

- RenderUrl: The answer to ?format=url. The link needs no key: the URL is the secret.
  - `ok` boolean
  - `url` string, uri
  - `filename` string
  - `bytes` integer
  - `validUntil` string, date-time, nullable: An hour from now. null when object storage is configured on a public bucket, because a plain link there does not expire and saying it does would be a promise you might build on.

## Other responses

- `400`: Something in the request is wrong, and the message says what.
- `401`: No key, an unknown key or a revoked one.
- `402`: The ceiling of the plan, or a plan whose end date has passed. The message says which, and the hint points at /billing.
- `403`: The key is real and the organization is right, but the scope on it does not cover this. The message names the scope the key has, what that scope may do, and how to get a wider one, because the thing reading it is usually the thing that has to act on it.
- `404`: Not there, or not yours. Both give the same answer on purpose: a different one would let a caller find out which ids exist in another organization.
- `500`: A fault at our end, with a reference into the log.
- `502`: The rendering itself went wrong. The answer names the engine that was used.
- `503`: The queue cannot take this on, or a dependency this needs is not there. Retry-After is on it when waiting is the answer.

---

[API](https://skmtc.dev/furo/apis/morfee.md) · [All operations](https://skmtc.dev/furo/apis/morfee/llms.txt) · [OpenAPI document](https://skmtc.dev/furo/apis/morfee/revisions/83f95f89653f?raw)
