---
name: morfee
description: Make PDF documents from saved HTML templates plus JSON. Use when a task needs a finished document - a quote, an invoice, a work order, a certificate, a letter - rather than text in a chat. Covers listing templates, finding out which fields a template expects, rendering to PDF or to a link, and retrying safely.
---

# Morfee

Morfee turns a saved HTML template plus your JSON into a finished document. One
POST comes back as the PDF bytes, as a download link, or as the rendered HTML.

Everything the builder can do, this API can do. If you can call HTTP, you never
need the screen.

## Getting in

Every call carries an API key belonging to one organization. Either header works:

```
X-Api-Key: mrf_a1b2c3_...
Authorization: Bearer mrf_a1b2c3_...
```

A key reaches nothing outside its organization, and it carries a scope: `read`,
`render`, `write` or `full`. A key can make another key, never a wider one
(`POST /api/keys`). Making the *first* key is a person's job, on the Keys screen.

Base URL: the installation you were given. The public one is
`https://morfee.furo.solutions`.

## The shape of the thing

A **template** is HTML that was pasted in once, with **bindings**: "this element
takes its text from this field". The HTML itself is never rewritten, which is why
a template can be re-pasted without losing its bindings.

Every template carries a **sample JSON** - the data it was built against. That
sample is the honest description of what the template expects, and it is the
first thing to read before you invent a payload.

Templates can sit in **folders**, one level deep.

## The loop that matters

1. `GET /api/templates` - what is in the library. Paged: `limit`, `offset`,
   `nextOffset` is `null` on the last page.
2. `GET /api/templates/{id}` - one template including its sample JSON.
3. `POST /api/render` - the document.

Do not guess a template id. List them, match on the name, and if nothing matches,
say so rather than rendering the wrong template convincingly.

## Making a document

```http
POST /api/render?format=url
X-Api-Key: mrf_...
Content-Type: application/json

{ "templateId": "quote-a3f9c1", "data": { "customer": { "name": "Example Gardens" } } }
```

Three sources, exactly one per call:

- `templateId` + `data` - the everyday one.
- `hbs` + `data` - a template you hold but have not saved.
- `html` - finished HTML you built yourself; no data, no template.

Sending two of them is refused rather than guessed at.

`format` decides the answer:

| format | you get |
| --- | --- |
| `pdf` (default) | the PDF bytes |
| `url` | JSON with a download link, valid an hour |
| `html` | the rendered HTML, to check before making paper |

`format=url` is the easy one from a workflow tool: a link survives being passed
between steps, bytes do not.

A flat payload also works: anything that is not `templateId`, `template`,
`filename`, `format`, `hbs` or `html` is taken as the data.

## Retrying without paying twice

If a call times out you cannot tell whether the document was made. Put your own
key on it:

```
Idempotency-Key: 7d2f1a90-4c11-4a0e-9f0c-1d0a0c9b7e21
```

That render happens **once**, however many times the request arrives. A repeat
gets the first answer back byte for byte with `Idempotent-Replay: 1` on it, and
is not counted or charged again. Keys are kept 24 hours.

- Same key, different body → `422 idempotency_key_reused`. Use a fresh key.
- Same key, still running → `409 idempotency_in_progress`, wait for `Retry-After`.
- A render that failed frees its key, so your retry is not stuck on the failure.

Use one key per document you mean to make, and reuse it for every retry of that
same document.

## Knowing when to stop

Every answer says how much of the month is left:

```
RateLimit-Policy: "renders";q=2500;w=2678400
RateLimit: "renders";r=2417;t=1209600
```

`r` counts down to the hard ceiling, where rendering actually stops. Above the
included bundle documents keep working and cost money per document; at `r=0` they
stop. If you are making documents in a loop, read `r` and slow down before it,
not after.

## When something goes wrong

Every failure is `application/problem+json` with a stable `code`:

```json
{ "code": "limit_reached", "status": 402, "title": "The plan is used up", "detail": "..." }
```

**Branch on `code`, never on the sentence.** The sentences get rewritten; the
codes do not. The full list is at `/errors`. The ones worth handling:

| code | what to do |
| --- | --- |
| `invalid_template` | the template or the data is wrong. The sentence names the part |
| `not_found` | no such template *in this organization*. List them again |
| `limit_reached` | the plan is used up. Stop; a human has to act |
| `busy` | the render queue is full. Wait `retryAfter` seconds, then retry |
| `render_failed` | the document itself defeated the browser. Usually its own fault |
| `forbidden_scope` | the key is too narrow for this call |

`detail` is the sentence for a person. The old `error` field carries the same
sentence and is not going away.

## Templates, if you are writing one

Eight helpers exist and nothing else: `text`, `format`, `rows`, `condition`,
`calc`, `math`, `tree` and `descend`, plus `if`, `unless` and `each`. Anything else is
refused when the template is saved rather than when it renders.

`{{math value "percent" "10"}}` is the only arithmetic there is: one operation
(`percent`, `times`, `divide`, `plus`, `minus`) on one number. Use it for a
deposit or a VAT line rather than asking the caller to work the number out.
`{{format link "url"}}` percent-encodes a value that has to go inside another
link, such as a product URL inside a QR-code URL.

There are no partials, so a template cannot call itself. For a structure of
unknown depth use `{{#tree list "spec"}}` with `{{descend children}}` inside it:
that renders the same block one level down, with the same sorting and filtering,
as many levels as the data has. Do not copy the block under itself instead. That
is what the tree helper is for, it stops silently at whatever depth you typed,
and it produces a template nobody can read afterwards.

A template you save this way has no bindings, so the builder cannot rebuild it
and opens it read only, next to an explanation of what it does worked out from
the template itself. Saving something with no logic in it over such a template is
refused with `409 would_drop_logic`; pass `replaceLogic: true` if that is really
what you mean.

A document may not fetch `file://` or an internal network address. That is
refused on purpose: the browser that draws the PDF runs on the server.

## Talking to it as an assistant

`POST /mcp` is the same product over JSON-RPC, with the same API key in the same
header. Sixteen tools: reading the library, the fields a template expects, making
documents, folders, versions and the bin. Nothing in it deletes anything for
good - the bin is as far as it goes.

The HTTP API and MCP are the same thing underneath. Use MCP if your client speaks
it; use HTTP if you are writing the calls yourself.

It speaks the stateless revision (`MCP-Protocol-Version: 2026-07-28`): no
handshake, no session, every request stands on its own. Older revisions back to
`2024-11-05` still work, `initialize` still answers, and a client is answered on
the revision it asked for rather than corrected to the newest.

## Also worth knowing

- `GET /api/usage` - the plan, the ceilings, and what has been used this month.
- `GET /openapi.json` - every endpoint, derived from the server's own route
  table, so it cannot drift from what actually answers.
- `GET /docs.md` - the whole documentation as markdown.
