Folslate API

One HTTP endpoint that hosts a Markdown or HTML file at a public link. Anonymous uploads need no account and no token: the body is the document. Uploads post to api.folslate.com; the returned link resolves on fol.ink for 24 hours — or permanently when posted with an account key, in which case expires is null. Every endpoint below carries a tester: fill the credential box once, then send live calls from this page.

Try-it credentials

Fill once, used by every tester below. Kept in memory only — nothing is stored, and a reload forgets them.

Sign in on folslate.com first, then send session-only calls from this browser.

Every JSON response has the same four fields

Both Workers answer with success, message, data and error at every status. success is true only on a 2xx. message is human prose and carries no contract — branch on error.code, which is a closed set. data is null on every failure; error is null on every success. Failures that carry extra facts put them beside the code: accepts on a 415, maxBytes on a 413.

The one exception is GET /:id on fol.ink: its success is the stored document bytes rather than JSON, and its failures return this envelope like every other route. A HEAD /:id probe answers with headers only at every status, success or failure — there is no body to parse.

success · 201
{
  "success": true,
  "message": "Document stored",
  "data": {
    "url": "https://fol.ink/doc_01m0t1nhwdsz815jvq92q00fkf",
    "expires": "2026-08-25T14:09:26.070Z",
    "title": "Hello",
    "ownership": "fsk_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ"
  },
  "error": null
}
failure
{
  "success": false,
  "message": "Unsupported Content-Type",
  "data": null,
  "error": {
    "accepts": [
      "text/markdown",
      "text/html"
    ],
    "code": "unsupported_media_type"
  }
}

Anonymous

No account and no token. Management calls in this group carry the ownership capability the upload returned.

GETAnonymousapi.folslate.com/

Service health

Reports that the Worker booted and its configuration is complete. It deliberately reaches no binding, so R2 and D1 are not probed. A degraded service answers 503 with the unbound names in error.missingBindings. It answers access-control-allow-origin: * with no credentials, so a browser page can read it from any origin.

Access

None. Unauthenticated and unmetered.

Success

200 — Served with cache-control: no-store.

Response data

statusok when every required binding is present, degraded otherwise.
serviceAlways folslate-api.
versionAlways v1.
missingBindingsRequired bindings that are absent. Non-empty only beside a 503.
warningsDegraded but serving: missing limiters, salts and secrets.
request
curl https://api.folslate.com/
response · 200
{
  "success": true,
  "message": "Service is healthy",
  "data": {
    "status": "ok",
    "service": "folslate-api",
    "version": "v1",
    "missingBindings": [],
    "warnings": []
  },
  "error": null
}
try it

POSTAnonymousapi.folslate.com/v1/upload

Host a document

Anonymous. No token. The body is the raw file bytes: text/markdown is rendered to HTML and wrapped in a titled page, text/html is sanitized and kept. Use --data-binary, never -d — -d strips newlines and collapses the whole file into one paragraph. The response carries an ownership capability that authorizes updating the document until it expires. With a session cookie or a Bearer fsa_ key the same route stores a permanent owned document instead: no capability is minted and data.expires is null.

Access

Optional. Bearer fsa_… or the session cookie for a permanent owned upload; without either the upload is anonymous and temporary.

Rate limit: 20/min per IP, shared across the upload, update and revoke paths. Answers 429 with Retry-After.

Headers

Content-Typetext/markdown or text/html; it decides the pipeline. Any other value answers 415 with error.accepts listing both.
X-Folslate-TitleOptional. Overrides the derived title. A CLI or agent carries UTF-8 in the header; a browser fetch() cannot, so put the title in the document instead.
AuthorizationOptional. Bearer fsa_… for a permanent owned upload. A malformed, revoked or unknown key — or an fsk_ capability — answers 401 and stores nothing.

Raw file bytes, not JSON. text/markdown renders to HTML; text/html is sanitized and kept.

Success

201 — data.url resolves on fol.ink. data.expires is the exact instant reads start answering 404 — hand it to the user beside the link — or null for a permanent owned document, which never expires. data.ownership is shown once and never stored in plaintext; save it beside the link if the document may need updating. Owned uploads carry no ownership field at all. A 503 can arrive after the bytes were stored, and it issued no capability — a retry mints a fresh document.

Response data

urlPublic fol.ink link for the document.
expiresISO-8601 instant reads start answering 404, or null for a permanent owned document.
titleDerived or overridden title.
ownershipBearer fsk_ capability for updating. Anonymous uploads only, shown once.
request
curl -X POST https://api.folslate.com/v1/upload \
  -H "Content-Type: text/markdown" \
  --data-binary @notes.md
body
# Hello

Write the document here.
response · 201
{
  "success": true,
  "message": "Document stored",
  "data": {
    "url": "https://fol.ink/doc_01m0t1nhwdsz815jvq92q00fkf",
    "expires": "2026-08-25T14:09:26.070Z",
    "title": "Hello",
    "ownership": "fsk_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

Stores a real document at a public link.

API key is not filled in the credential box above.

PUTAnonymousapi.folslate.com/v1/documents/:id

Replace a document

Replaces the document at the same fol.ink link. Authorized by the ownership capability the upload returned — nothing else proves ownership, and the public URL alone grants nothing — or by the owning account's session cookie or Bearer fsa_ key. A non-owner answers 404, the same answer a stranger gets for a revoked document. The body follows the same rules as an upload, including the title pipeline, and an authorized anonymous update restarts the 1-day retention window, which the echoed expiry reflects. An owned document stays permanent and echoes expires: null.

Access

Bearer <ownership> capability, or the owning account's session cookie or Bearer fsa_ key.

Rate limit: 20/min per IP on the update path, shared with uploads and revokes. Answers 429 with Retry-After.

Headers

AuthorizationBearer <ownership> or Bearer fsa_… — the capability from the upload's 201, or the owning account's key. Sent only to api.folslate.com; the capability was shown once at upload and is never recoverable.
Content-Typetext/markdown or text/html, deciding the pipeline again for the new bytes.
X-Folslate-TitleOptional. Overrides the derived title of the replacement document.

Raw file bytes, not JSON. Follows the same pipeline as an upload.

Success

200 — The capability is not re-issued — only the upload response carries it. An owned update echoes expires: null. The replacement is immediate at the origin; readers that fetched earlier keep their cached copy for up to its remaining max-age. 404 covers unknown, expired, revoked, foreign and deleted documents, indistinguishably.

Response data

urlUnchanged public fol.ink link.
expiresRestarted 1-day expiry for anonymous documents, null for owned ones.
titleTitle of the replacement.
request
curl -X PUT https://api.folslate.com/v1/documents/doc_01m0t1nhwdsz815jvq92q00fkf \
  -H "Authorization: Bearer fsk_…" \
  -H "Content-Type: text/markdown" \
  --data-binary @notes-revised.md
body
# Hello, revised

Write the replacement here.
response · 200
{
  "success": true,
  "message": "Document updated",
  "data": {
    "url": "https://fol.ink/doc_01m0t1nhwdsz815jvq92q00fkf",
    "expires": "2026-08-26T14:09:26.070Z",
    "title": "Hello, revised"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsk_… from the upload's 201
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

Replaces the live document for real. There is no undo.

Fill every :path parameter to send.

Capability is not filled in the credential box above.

DELETEAnonymousapi.folslate.com/v1/documents/:id/ownership

Revoke the ownership capability

Invalidates the capability so the document can no longer be updated or re-revoked by anyone, its holder included. Revocation is irreversible: only a one-way hash is stored, so the capability cannot be re-issued. The document itself keeps serving until its expiry — this endpoint never takes bytes down.

Access

Bearer <ownership> — the capability from the upload's 201.

Rate limit: 20/min per IP on the revoke path, shared with uploads and updates. Answers 429 with Retry-After.

Headers

AuthorizationBearer <ownership> — the capability from the upload's 201.

Success

200 — A 503 means the capability is still live; retry — either the ownership lookup failed or the revocation write did, and both are authoritative. After a 200 the document is unmanageable for its remaining life.

Response data

idDocument id whose capability died.
request
curl -X DELETE https://api.folslate.com/v1/documents/doc_01m0t1nhwdsz815jvq92q00fkf/ownership \
  -H "Authorization: Bearer fsk_…"
response · 200
{
  "success": true,
  "message": "Ownership capability revoked",
  "data": {
    "id": "doc_01m0t1nhwdsz815jvq92q00fkf"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsk_… from the upload's 201

Fill every :path parameter to send.

Capability is not filled in the credential box above.

GETAnonymousapi.folslate.com/v1/report/:id

Describe the abuse channel

Describes the channel and records nothing. Link unfurlers, prefetchers and security scanners all issue GETs, so the read path is deliberately write-free.

Access

None. Unauthenticated and unmetered.

Success

200 — reportTo is the monitored mailbox operators can write to directly.

Response data

idEchoed document id.
reportToMonitored mailbox for direct reports.
howToHow to file: POST here with ?reason=.
request
curl https://api.folslate.com/v1/report/doc_…
response · 200
{
  "success": true,
  "message": "Abuse report channel",
  "data": {
    "id": "doc_…",
    "reportTo": "abuse@folslate.com",
    "howTo": "POST to this URL with an optional ?reason= to file a report"
  },
  "error": null
}
try it

Fill every :path parameter to send.

POSTAnonymousapi.folslate.com/v1/report/:id

File an abuse report

Files a report against a document. The write is awaited and result-bearing: a 202 means the record was kept, and 503 means it was not.

Access

None. Anyone can report any link.

Rate limit: 5/min per IP in its own namespace, answers 429 with Retry-After. Report traffic cannot spend the upload quota.

Query parameters

reasonOptional. Capped at 500 characters.

Success

202 — success: true beside a 202 is the acknowledgement; there is no received field.

Response data

idReported document id.
request
curl -X POST "https://api.folslate.com/v1/report/doc_…?reason=phishing"
response · 202
{
  "success": true,
  "message": "Abuse report received",
  "data": {
    "id": "doc_…"
  },
  "error": null
}
try it

Files a real report against this document.

Fill every :path parameter to send.

Auth

Email-code sign-in, no passwords. The live routes answer their own JSON shapes, not the Folslate envelope.

POSTAuthapi.folslate.com/v1/auth/email-otp/send-verification-otp

Send a sign-in code

Step one of sign-in. Mails a 6-digit code valid for 5 minutes. Unknown addresses receive the same success shape, so the endpoint never reveals which addresses hold accounts. Send type sign-in: type forget-password answers the enveloped 404, and the password-reset and email-change paths below are not sign-in routes at all.

Access

None. This is how a session starts.

Rate limit: 5/min per IP, answers 429 with Retry-After. The preflight itself never spends quota.

Headers

Content-Typeapplication/json carrying the email body.

Body fields

emailAddress to mail the code to.
typeAlways "sign-in". Any other value answers 404.

JSON body. The mail carries nothing but the code.

Success

200 — Plain JSON, not the Folslate envelope: { success: true } with no error.code. Delivery runs in the background, so a 200 means the code was minted, not that the mail landed.

Response data

successAlways true on a 200. Check the mail next.
request
curl -X POST https://api.folslate.com/v1/auth/email-otp/send-verification-otp \
  -H "Content-Type: application/json" \
  --data '{"email":"you@example.com","type":"sign-in"}'
body
{
  "email": "you@example.com",
  "type": "sign-in"
}
response · 200
{
  "success": true
}
try it

Sends a real email with a real code.

POSTAuthapi.folslate.com/v1/auth/sign-in/email-otp

Sign in with the code

Step two of sign-in. Exchanges the emailed code for a session: the response sets the session cookie and echoes the token and user as JSON. The first sign-in creates the account. Three wrong attempts invalidate the code.

Access

None. The emailed code is the credential.

Rate limit: 5/min per IP, answers 429 with Retry-After. Wrong codes also burn attempts against the code itself: three strikes invalidates it.

Headers

Content-Typeapplication/json carrying the email and code body.

Body fields

emailSame address the code went to.
otp6-digit code from the mail.

JSON body. Save nothing from it: the session lives in the cookie the response sets.

Success

200 — Plain JSON, not the Folslate envelope: the body carries the session token and user, and the response sets the session cookie. The cookie domain comes from configuration, so localhost carries none.

Response data

tokenSession token, mirrored by the cookie.
userSigned-in user: id, email, plan and timestamps.
request
curl -X POST https://api.folslate.com/v1/auth/sign-in/email-otp -c cookies.txt \
  -H "Content-Type: application/json" \
  --data '{"email":"you@example.com","otp":"123456"}'
body
{
  "email": "you@example.com",
  "otp": "123456"
}
response · 200
{
  "token": "<session-token>",
  "user": {
    "id": "…",
    "email": "you@example.com",
    "name": "",
    "emailVerified": true,
    "plan": "free",
    "createdAt": "2026-09-07T16:03:50.000Z",
    "updatedAt": "2026-09-07T16:03:50.000Z"
  }
}
try it

Stores the session cookie in this browser. Tick Use session cookie above before sending the calls below.

POSTAuthapi.folslate.com/v1/auth/sign-out

Sign out

Ends the session the cookie carries. Afterwards the cookie authenticates nothing: GET /v1/me with it answers 401.

Access

Session cookie. Send the browser's cookies with the call.

Rate limit: 5/min per IP like every auth POST, answers 429 with Retry-After.

Headers

CookieSession cookie from sign-in. The call ends that session.
Content-Typeapplication/json. Required even though the body says nothing.

An empty JSON object. The cookie names the session, so the body carries no field, but the auth mount still parses it: omit the header and the call answers 415, send the header without a body and it answers 400.

Success

200 — Plain JSON, not the Folslate envelope: { success: true }. The session row is gone; a replayed cookie answers 401.

Response data

successAlways true on a 200. The session is over.
request
curl -X POST https://api.folslate.com/v1/auth/sign-out -b cookies.txt \
  -H "Content-Type: application/json" \
  --data '{}'
body
{}
response · 200
{
  "success": true
}
try it
Send as
— missing: This browser's folslate.com session

Session cookie is not filled in the credential box above.

POSTAuthapi.folslate.com/v1/auth/* (blocked paths)

Paths that answer 404 by design

Password-reset and email-change paths exist on the auth mount, but Folslate excludes credential and email-mutation flows. Those paths answer the enveloped 404 from an explicit route in front of the mount — a 404 here means unsupported, not missing. The send-code endpoint with type forget-password answers the same way.

Access

None. These paths have no function to authorize.

Documented answer

404 — The Folslate envelope with error.code not_found — unlike the live auth routes, which answer their own shapes.

Response data

error.codeAlways not_found on these paths.
request
curl -X POST https://api.folslate.com/v1/auth/email-otp/request-password-reset
response · 404
{
  "success": false,
  "message": "The requested resource does not exist or has expired",
  "data": null,
  "error": {
    "code": "not_found"
  }
}
try it

Account

Session cookie or Bearer fsa_ key. An fsk_ capability answers 401 here.

GETAccountapi.folslate.com/v1/documents

List owned documents

Lists the calling account's documents newest first, with cursor pagination. Deleted rows and other users' rows never appear. Needs a session cookie or a Bearer fsa_ key.

Access

Session cookie or Bearer fsa_…. An fsk_ capability answers 401 here.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates. An fsk_ capability answers 401 here.

Query parameters

cursorOptional. A document id; the page starts after it. Omit for the first page.
limitOptional. Items per page, default 50, maximum 100.

Success

200 — nextCursor is the cursor for the following page, or null on the last page. createdAt and updatedAt are ISO-8601; updatedAt is null until the first PUT.

Response data

items[]One entry per document: id, url, title, contentType, sourceBytes, renderedBytes, createdAt, updatedAt.
nextCursorDocument id to pass as cursor for the next page, or null on the last page.
request
curl https://api.folslate.com/v1/documents -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "Documents",
  "data": {
    "items": [
      {
        "id": "doc_01m0t1nhwdsz815jvq92q00fkf",
        "url": "https://fol.ink/doc_01m0t1nhwdsz815jvq92q00fkf",
        "title": "Hello",
        "contentType": "text/markdown",
        "sourceBytes": 12,
        "renderedBytes": 3456,
        "createdAt": "2026-08-25T14:09:26.000Z",
        "updatedAt": null
      }
    ],
    "nextCursor": null
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

API key is not filled in the credential box above.

GETAccountapi.folslate.com/v1/documents/:id

Inspect an owned document

Returns one owned document's record with its lifetime view count — rolled daily totals plus unrolled raw events. Foreign, deleted and unknown ids answer 404, indistinguishably. Needs a session cookie or a Bearer fsa_ key.

Access

Session cookie or Bearer fsa_…. An fsk_ capability answers 401 here.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates.

Success

200 — views counts every served read; HEAD probes are excluded. createdAt and updatedAt are ISO-8601.

Response data

idDocument id.
urlPublic fol.ink link.
titleDerived or overridden title.
contentTypetext/markdown or text/html.
sourceBytesUploaded bytes.
renderedBytesStored bytes.
createdAtISO-8601 upload instant.
updatedAtISO-8601 last update, null until the first PUT.
viewsLifetime served reads. HEAD probes are excluded.
request
curl https://api.folslate.com/v1/documents/doc_… -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "Document",
  "data": {
    "id": "doc_01m0t1nhwdsz815jvq92q00fkf",
    "url": "https://fol.ink/doc_01m0t1nhwdsz815jvq92q00fkf",
    "title": "Hello",
    "contentType": "text/markdown",
    "sourceBytes": 12,
    "renderedBytes": 3456,
    "createdAt": "2026-08-25T14:09:26.000Z",
    "updatedAt": null,
    "views": 9
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

Fill every :path parameter to send.

API key is not filled in the credential box above.

DELETEAccountapi.folslate.com/v1/documents/:id

Delete an owned document

Tombstones the row and removes the bytes, so the link stops resolving. The row is marked first and the R2 object deleted after; an R2 failure after the mark answers 503 with the row already tombstoned. A retry deletes the leftover object and answers 200; a delete once the object is gone answers 404. Needs a session cookie or a Bearer fsa_ key.

Access

Session cookie or Bearer fsa_…. An fsk_ capability answers 401 here.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates.

Success

200 — Deletion is permanent: the bytes are removed, not revoked. Revoking the capability instead keeps serving; see the ownership route.

Response data

idDeleted document id.
request
curl -X DELETE https://api.folslate.com/v1/documents/doc_… -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "Document deleted",
  "data": {
    "id": "doc_01m0t1nhwdsz815jvq92q00fkf"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

Fill every :path parameter to send.

API key is not filled in the credential box above.

GETAccountapi.folslate.com/v1/usage

Usage against the plan limits

Returns the calling account's plan, its limits, and current usage: uploads and requests since UTC midnight, live document and key counts, and stored bytes. Needs a session cookie or a Bearer fsa_ key.

Access

Session cookie or Bearer fsa_…. An fsk_ capability answers 401 here.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates.

Success

200 — resetsAt is the next UTC midnight, when the daily counters restart. Active and storage figures never reset, so a quota error on one of those carries resetsAt null.

Response data

planCalling account's plan.
limitsPlan row: activeDocuments, uploadsPerDay, requestsPerDay, storageBytes, activeKeys.
usageuploadsToday and requestsToday since UTC midnight; activeDocuments, storageBytes and activeKeys as live counts.
resetsAtNext UTC midnight, when daily counters restart.
request
curl https://api.folslate.com/v1/usage -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "Usage",
  "data": {
    "plan": "free",
    "limits": {
      "activeDocuments": 100,
      "uploadsPerDay": 50,
      "requestsPerDay": 2000,
      "storageBytes": 104857600,
      "activeKeys": 10
    },
    "usage": {
      "uploadsToday": 3,
      "requestsToday": 41,
      "activeDocuments": 7,
      "storageBytes": 123456,
      "activeKeys": 2
    },
    "resetsAt": "2026-09-07T00:00:00.000Z"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

API key is not filled in the credential box above.

GETAccountapi.folslate.com/v1/me

Who am I

Returns the calling account. Accepts a session cookie or a Bearer fsa_ key, so a key checks itself here without spending a document. The call itself is metered like any authenticated request. A failed session lookup answers 503 while a valid key still succeeds.

Access

Session cookie or Bearer fsa_…. The cheapest way to check a key.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates.

Success

200 — createdAt is ISO-8601. plan is free until pricing lands.

Response data

idAccount id.
emailSign-in address.
planPlan name, free until pricing lands.
createdAtISO-8601 account creation.
request
curl https://api.folslate.com/v1/me -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "Current user",
  "data": {
    "id": "…",
    "email": "you@example.com",
    "plan": "free",
    "createdAt": "2026-09-07T16:03:50.000Z"
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

API key is not filled in the credential box above.

GETAccountapi.folslate.com/v1/keys

List API keys

Lists the calling account's active keys, newest first. Revoked keys never appear, and the plaintext never does either. Accepts a session cookie or a Bearer fsa_ key.

Access

Session cookie or Bearer fsa_….

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

AuthorizationBearer fsa_… unless the session cookie authenticates.

Success

200 — lastUsedAt is null until the key authenticates its first request; createdAt is ISO-8601.

Response data

items[]One entry per active key: id, name, hint, createdAt, lastUsedAt. Never the plaintext.
request
curl https://api.folslate.com/v1/keys -H 'Authorization: Bearer fsa_…'
response · 200
{
  "success": true,
  "message": "API keys",
  "data": {
    "items": [
      {
        "id": "key_01m0t1nhwdsz815jvq92q00fkf",
        "name": "agent",
        "hint": "EOOQ",
        "createdAt": "2026-09-07T16:14:21.000Z",
        "lastUsedAt": null
      }
    ]
  },
  "error": null
}
try it
Send as
— missing: Bearer fsa_… from the credential box
— missing: This browser's folslate.com session

API key is not filled in the credential box above.

Session-only

Session cookie only. A Bearer key answers 403, so a leaked key cannot mint or revoke its successors.

POSTSession-onlyapi.folslate.com/v1/keys

Mint an API key

Mints one named key for the calling account. Session cookie only: a Bearer key answers 403, so a leaked key cannot mint its successors. The body is { name }, 1 to 64 characters with blank names rejected. At 10 active keys the answer is 429 with metric activeKeys. The plaintext is shown once and never stored; only its hash is kept.

Access

Session cookie only. A Bearer key answers 403 session_required.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

CookieSession cookie from sign-in. A Bearer key is refused here.
Content-Typeapplication/json carrying { name }.

Body fields

nameKey name, 1 to 64 characters. Blank names are rejected.

JSON body carrying the key name.

Success

201 — data.key is the only time the plaintext is shown; save it beside the id. hint is its last four characters, for recognizing the key in the list.

Response data

idKey id, key_… prefixed.
nameEchoed key name.
keyPlaintext fsa_ secret. Shown once, never stored.
hintLast four plaintext characters.
createdAtISO-8601 mint instant.
request
curl -X POST https://api.folslate.com/v1/keys -b cookies.txt \
  -H "Content-Type: application/json" \
  --data '{"name":"agent"}'
body
{
  "name": "agent"
}
response · 201
{
  "success": true,
  "message": "API key created",
  "data": {
    "id": "key_01m0t1nhwdsz815jvq92q00fkf",
    "name": "agent",
    "key": "fsa_A1accYsYJrk83UVKwpjlsflMcWkruQbQTC0_cQcEOOQ",
    "hint": "EOOQ",
    "createdAt": "2026-09-07T16:14:21.000Z"
  },
  "error": null
}
try it
Send as
— missing: This browser's folslate.com session

Mints a real key on the account.

Session cookie is not filled in the credential box above.

DELETESession-onlyapi.folslate.com/v1/keys/:id

Revoke an API key

Revokes one key now. Unknown, foreign, or malformed ids answer 404. Session cookie only, like minting.

Access

Session cookie only. A Bearer key answers 403 session_required.

Rate limit: 60/min per user plus the daily requests quota. Answers 429 when either is spent.

Headers

CookieSession cookie from sign-in. A Bearer key is refused here.

Success

200 — Revocation is permanent and immediate: the next request with the key answers 401.

Response data

idRevoked key id.
request
curl -X DELETE https://api.folslate.com/v1/keys/key_… -b cookies.txt
response · 200
{
  "success": true,
  "message": "API key revoked",
  "data": {
    "id": "key_01m0t1nhwdsz815jvq92q00fkf"
  },
  "error": null
}
try it
Send as
— missing: This browser's folslate.com session

Fill every :path parameter to send.

Session cookie is not filled in the credential box above.

Reader

Public and unauthenticated. Success is the stored document bytes with headers only, never JSON.

GETReaderfol.ink/:id

Read a document

Public and unauthenticated. Returns the stored HTML bytes — the one success that is not JSON. A failed GET answers 404 or 503 with the JSON envelope under content-type application/json; tell failure from document by status and content type, never by the body's first character, which a valid document may start with. A read past the expiry answers 404 even while the bytes are still in the bucket.

Access

None. The link is the whole credential.

Success

200 — Cache-Control carries the document's remaining lifetime, not a fixed window.

Response headers

content-security-policyInert-page policy from §4. No sandbox.
content-typeThe stored document type.
cache-controlRemaining lifetime, not a fixed window.
request
curl -fsS https://fol.ink/doc_…
response · 200
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'none'; script-src 'sha256-blo5hYNaQKIRflegUj5xXDxymQmEqDztupWN7IXtzGU='; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'none'
Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff
try it

fol.ink sends no CORS headers, so the browser hides the response. The call is still sent — use Open link for GET, curl otherwise.

Fill every :path parameter to send.

HEADReaderfol.ink/:id

Probe a document

Same route and headers as the read, with no body. A HEAD probe is not recorded as a view, which makes it the way to check existence or headers without spending an analytics event.

Access

None. The link is the whole credential.

Success

200 — Headers only — no body at any status, success or failure. Does not record a view.

Response headers

content-security-policyInert-page policy from §4. No sandbox.
content-typeThe stored document type.
cache-controlRemaining lifetime, not a fixed window.
request
curl -I https://fol.ink/doc_…
response · 200
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'none'; script-src 'sha256-blo5hYNaQKIRflegUj5xXDxymQmEqDztupWN7IXtzGU='; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'none'
Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff
try it

fol.ink sends no CORS headers, so the browser hides the response. The call is still sent — use Open link for GET, curl otherwise.

Fill every :path parameter to send.

Errors

error.code is drawn from a closed set shared by both Workers. Statuses and prose live in one table, so two handlers cannot answer the same failure differently. internal_error can surface on any route; request_rejected covers rejections that carry no more specific code. Both are also the only codes whose status can move: a thrown HTTPException keeps its own status, so the values below for those two are catalogue defaults, not guarantees.

error.codeStatusMessageWhat to do
not_found404The requested resource does not exist or has expiredOn a read: expired, never existed, or a malformed id. These are deliberately indistinguishable, so do not retry.
unsupported_media_type415Unsupported Content-TypeSend text/markdown or text/html. error.accepts lists the accepted types.
payload_too_large413Document exceeds the maximum upload sizeThe body is over error.maxBytes. Split the document or trim it.
unprocessable_document422The document could not be renderedRendering failed. The same bytes fail the same way, so change them before retrying.
rate_limited429Too many requestsWait the Retry-After seconds, then retry. Counters run per IP, per Cloudflare location.
unauthorized401A valid ownership capability or user credential is requiredThe Authorization header is missing, malformed, or carries a capability this document does not match. Nothing else authorizes document management.
storage_unavailable503Document storage is temporarily unavailableTransient. Retry once.
report_not_recorded503The abuse report was not recordedThe report was not kept. Retry, and treat a 503 as authoritative — the endpoint never acknowledges a report it did not store.
revocation_not_recorded503The ownership capability was not revokedThe capability is still live. Retry; treat a 503 as authoritative, exactly like a report 503.
degraded503Service configuration is incompleteThe service is running with required bindings missing. Nothing a client can do; the binding names are in error.missingBindings.
configuration_error503Service configuration is incompleteA required origin variable is unusable. Not a client error; nothing to retry.
request_rejected400 (default)The request was rejectedThe request was rejected without a more specific code. Check the URL, method and body.
session_required403Key management needs a session, never a keyKey management needs a session cookie, never a Bearer key. Sign in and retry with the cookie.
quota_exceeded429Quota exceededThe plan quota in error.metric is spent. For activeKeys, revoke a key; for the daily uploadsPerDay and requestsPerDay quotas, wait for error.resetsAt; for activeDocuments and storageBytes, delete or reduce stored documents — error.resetsAt is null there, so nothing resets on its own.
internal_error500 (default)Unexpected server errorUnexpected server error. Retry once; if it persists, only the operator's logs can say why — GET / reports configuration, not runtime health.

Limits

Max upload size
1 MB. Larger bodies answer 413 with error.maxBytes.
Retention
24 hours from upload for anonymous documents. Reads past the expiry answer 404, and the bytes are deleted on a lagging schedule after it. A key-authenticated upload is permanent: expires is null and the bytes match no lifecycle rule.
Upload rate limit
20/min per IP, shared across the upload, update and revoke paths, answers 429 with Retry-After.
Report rate limit
5/min per IP, in its own namespace — report traffic cannot spend the upload quota.
Auth rate limit
5/min per IP on auth POSTs, behind the preflight so the preflight never spends quota.

The same API written for agents is served as three skills: folslate.com/skills/folslate-api.md, folslate.com/skills/folslate-cli.md, and folslate.com/skills/create-artifact.md, alongside the single-file folslate.com/skill.md.