---
name: folslate
description: Upload a Markdown or HTML file to Folslate and get a public link on fol.ink. Use when sharing a generated report or page as a URL, when output is too long to paste inline, or when reading a fol.ink link back.
---

# Folslate

Folslate hosts one Markdown or HTML file at a public URL. An anonymous upload
needs no account and no token: POST the bytes to `api.folslate.com`, get back
a `fol.ink` link and an ownership secret that authorizes updating the document
until it expires. A key-authenticated upload (`Authorization: Bearer fsa_…`)
is permanent instead: it mints no ownership secret and its `201` carries
`expires: null`.

## Check these before uploading

None of the three can be undone after the upload.

**The link is public.** Anyone holding the URL reads the document, with no
authentication. Folslate has no list endpoint and no takedown, so a link
cannot be deleted or unpublished once it is given out. Keep credentials, keys,
customer records, and anything the user has not agreed to publish out of the
upload.

**It expires in one day — unless the upload was key-authenticated.** The `201`
carries the exact `expires` timestamp, or null for a permanent owned document.
Hand it to the user beside the link. After it, reads answer `404`. A permanent
document never lapses and stays manageable from the owning account: list, get,
update and delete live under `GET /v1/documents`, `GET /v1/documents/:id`,
`PUT /v1/documents/:id` and `DELETE /v1/documents/:id` with the same key.

**The page is inert.** A hosted document cannot run JavaScript, load an
external stylesheet, font, or image, submit a form, or be framed. An HTML
report that pulls a chart library from a CDN renders as a blank page. Folslate
also strips every `<meta http-equiv>` and every `<noscript>` at upload.

What you can do instead depends on the upload type. A `text/html` upload keeps
inline `<style>`, and images load as `data:` URIs, so inline the SVG the CDN
would have drawn. A `text/markdown` upload escapes raw HTML rather than passing
it through, so an `<svg>` or `<style>` block written into Markdown arrives as
visible text; Folslate styles Markdown itself, and a chart has to become a
`data:` image.

## Upload

```bash
curl -sS -X POST https://api.folslate.com/v1/upload \
  -H 'content-type: text/markdown' \
  --data-binary @report.md
```

Use `--data-binary`, never `-d`. `-d` strips newlines and collapses the whole
file into one paragraph, which destroys Markdown.

`content-type` is `text/markdown` or `text/html`. Folslate rejects every other
value with `415`, and it decides the pipeline: a `text/markdown` body is
converted to HTML and wrapped in a page shell, a `text/html` body is sanitized
and kept.

A `201` looks like this:

```json
{
    "success": true,
    "message": "Document stored",
    "data": {
        "url": "https://fol.ink/doc_01k3n8w5q2r7v0xyz4a6bcdefg",
        "expires": "2026-08-26T09:12:44.000Z",
        "title": "Quarterly report",
        "ownership": "fsk_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ"
    },
    "error": null
}
```

Report `data.url` and `data.expires` together. A link without its expiry reads
as permanent, and an anonymous one is not. An `expires: null` answers that
question the other way: the document is permanent. A key-authenticated `201`
carries no `ownership` field at all — the owning account manages the document
instead, so there is no secret to save.

`data.ownership` is shown exactly once and never stored in readable form: it
authorizes updating the document until expiry, and losing it means losing that
ability. Save it beside the link only if the document may need updating, and
never send it to anyone but `api.folslate.com`.

## Update or revoke a document

An authorized update replaces the document at the same link. The body follows
the same rules as an upload, the title pipeline runs again, and the 1-day
retention window restarts at the update, which the echoed `expires` reflects.
An owned document stays permanent and echoes `expires: null`. Readers that
fetched the document before the update keep their cached copy for
up to its remaining lifetime, so a change reaches new reads immediately and
existing readers by the old expiry:

```bash
curl -sS -X PUT https://api.folslate.com/v1/documents/doc_01k3n8w5q2r7v0xyz4a6bcdefg \
  -H 'authorization: Bearer fsk_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ' \
  -H 'content-type: text/markdown' \
  --data-binary @report-revised.md
```

Revoking the secret makes the document unmanageable for its remaining life, to
its holder too and irreversibly. The document itself keeps serving until
expiry; revocation is not a takedown. A 503 means the capability is still
live — retry once:

```bash
curl -sS -X DELETE https://api.folslate.com/v1/documents/doc_01k3n8w5q2r7v0xyz4a6bcdefg/ownership \
  -H 'authorization: Bearer fsk_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ'
```

Both answer `401` when the capability is missing or wrong and `404` when the
document is unknown, expired, or already revoked — indistinguishable on
purpose. An owned document answers `404` to any `fsk_` here; manage it with
the account key instead. There is no way to recover a lost `ownership` value.

## Read a document back

```bash
curl -fsS https://fol.ink/doc_01k3n8w5q2r7v0xyz4a6bcdefg
```

A live document answers `200` with `content-type: text/html` and the stored
HTML. A failed GET answers `404` or `503` with `content-type: application/json`
and the JSON envelope. Tell them apart by status and content type, never by
the body's first character — a valid HTML document may begin with `{`.

`curl -I` on the same URL tells you a link is alive without recording a view.
HEAD answers headers only — it has no body at any status, so read its status
code rather than expecting a JSON envelope.

Ids are `doc_` followed by 26 lowercase Crockford Base32 characters, which
exclude `i`, `l`, `o`, and `u`. Use the `url` the upload returned rather than
building one.

## Titles

Every stored document carries a `<title>`. Folslate takes it from
`X-Folslate-Title`, else the document's own `<title>` or first heading, else
the document id.

```bash
curl -sS -X POST https://api.folslate.com/v1/upload \
  -H 'content-type: text/html' \
  -H 'x-folslate-title: Release notes' \
  --data-binary @page.html
```

The `201` echoes the title the document actually got, so read `data.title`
rather than fetching the document back to check. Folslate collapses whitespace
and cuts the title at 200 characters, so a long one comes back changed.

A header value carries UTF-8 correctly from a CLI or an agent. Browser
`fetch()` cannot reliably send a non-ASCII header, so in browser code put the
title in the document instead, where a Markdown `#` heading is read from the
body as UTF-8.

## When it fails

Every response from `api.folslate.com`, and every failed GET on either host,
has the same four fields: `success`, `message`, `data`, `error`. `data` is `null`
whenever `success` is `false`. Branch on `error.code`. The `message` is prose
and gets reworded. A `HEAD` probe is the exception: it returns no body at any
status, so branch on its status code.

| `error.code`             | Status | What to do                                                                                                       |
| ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `unauthorized`           | 401    | The `authorization` header is missing, malformed, or wrong. Only the capability authorizes management.            |
| `unsupported_media_type` | 415    | Send `text/markdown` or `text/html`. `error.accepts` lists them.                                                 |
| `payload_too_large`      | 413    | The body is over `error.maxBytes`. Split the document or trim it.                                                |
| `unprocessable_document` | 422    | Rendering failed. The same bytes fail the same way, so change them before retrying.                              |
| `rate_limited`           | 429    | Wait the `Retry-After` seconds, then retry.                                                                      |
| `storage_unavailable`    | 503    | Transient. Retry once. A retried upload mints a new document; a retried update re-applies the same bytes.                                        |
| `revocation_not_recorded` | 503   | The capability is still live. Retry; a 503 here is authoritative, like a report 503.                             |
| `not_found`              | 404    | On a read: expired, never existed, or a malformed id. On management: also revoked. Deliberately indistinguishable, so do not retry. |

## Limits

Max body is 1 MB. Uploads and document updates share a rate limit per IP of
20 a minute (revocations share it too), counted per Cloudflare location rather
than globally, so treat it as a brake on bulk traffic rather than an exact
quota. Retention is one day from the last upload or update — except a
key-authenticated upload, which is permanent and answers `expires: null`.

The full human-readable reference, with every endpoint, error code and limit,
is at <https://folslate.com/api>.
