Collections
Mark any folder as a collection and Hyperclay turns it into a place to gather submissions: a public form anyone can fill out, and a private dashboard only you can see. Every submission is saved as a JSON record inside your own folder, so the data is yours and travels with your account.
Hyperclay seeds a working form and dashboard for you, but they are just plain HTML files. You can edit them, swap them out, or build your own from scratch against the same API. This page is that API.
Mark a folder as a collection
In your file manager, open a folder’s menu and choose Mark as Collection. Hyperclay adds three things inside it:
submission-form.html, a public form,dashboard.html, an owner-only dashboard,- a
records/folder, where submissions are stored as JSON.
To build your own pages you need two values. Open the collection’s menu and choose Collection info:
- Collection ID (looks like
col_a1b2c3xy): the public id for the API. It is stable, so it never changes even if you rename or move the folder. - Submit token: the credential a public form sends to create a submission. You only need this if you build your own form.
The API
Every endpoint is JSON over the same origin as your site. Send and read application/json. A plain HTML form post will not work, you submit with fetch.
| Method | Path | Who | Purpose |
|---|---|---|---|
| POST | /_/collection/<id>/records | anyone with the token | create or update a submission |
| GET | /_/collection/<id>/records?limit=&offset= | owner only | list submissions |
| GET | /_/collection/<id>/records/<key> | depends on the record | read one submission |
| PUT | /_/collection/<id>/records/<key> | owner or the record’s owner | update one submission |
| DELETE | /_/collection/<id>/records/<key> | owner only | delete one submission |
<id> is the Collection ID from the modal. In a list (the owner GET that returns records), each record comes back as:
{ "id": "...", "data": { }, "modifiedAt": "2026-06-01T...", "size": 128 }A single-record read returns { "id": "...", "data": { }, "modifiedAt": "2026-06-01T..." }, without size. In both cases data is whatever JSON you submitted. Success responses are { "ok": true, ... }. Failures return a non-2xx status with { "ok": false, "error": "..." }.
Limits: 1MB per submission, 30 requests per minute per IP, and 10,000 records per collection. Records count toward your account’s file storage.
Build your own form
The simplest public form collects some fields and POSTs them as data with the submit token. Hyperclay gives each submission an unguessable id and remembers it in a cookie, so the same browser can come back and edit its own submission later.
<form id="form">
<input name="name" placeholder="Your name">
<textarea name="message" placeholder="Your message"></textarea>
<button>Send</button>
</form>
<script type="module">
const ID = 'col_a1b2c3xy' // Collection ID, from the folder's "Collection info"
const TOKEN = 'paste-token-here' // Submit token, same place
const form = document.getElementById('form')
form.addEventListener('submit', async (e) => {
e.preventDefault()
const data = Object.fromEntries(new FormData(form))
const res = await fetch(`/_/collection/${ID}/records`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token: TOKEN, data })
})
if (res.ok) form.replaceWith('Thanks, we got it.')
else alert('Something went wrong, please try again.')
})
</script>data is just an object. You decide its shape. There is no server-side schema to define.
Let submitters come back and edit (optional)
Pass a key you choose (an email, a phone number) plus a code (a secret the submitter picks). The record is then identified by the key and protected by the code: the submitter can reload it from any device by re-sending the key and code, and nobody else can read or change it. The code is never stored, only a hash of it.
body: JSON.stringify({ token: TOKEN, key: email, code: secret, data })To load an existing keyed record back into a form, GET it with the code in a header so it stays out of the URL:
const res = await fetch(`/_/collection/${ID}/records/${encodeURIComponent(email)}`, {
headers: { 'x-collection-code': secret }
})
const { ok, data } = await res.json()Build your own dashboard
A dashboard is even simpler, because when you (the owner) view a page on your own site, your login cookie rides along automatically. No token, no auth code:
<div id="list"></div>
<script type="module">
const ID = 'col_a1b2c3xy'
const res = await fetch(`/_/collection/${ID}/records?limit=200`)
const { records } = await res.json()
document.getElementById('list').innerHTML = records.length
? records.map(r => `<pre>${JSON.stringify(r.data, null, 2)}</pre>`).join('')
: 'No submissions yet.'
</script>Editing and deleting are one call each:
// update a record's data
await fetch(`/_/collection/${ID}/records/${encodeURIComponent(key)}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: updated })
})
// delete a record
await fetch(`/_/collection/${ID}/records/${encodeURIComponent(key)}`, { method: 'DELETE' })A PUT replaces the whole data object. If you are only changing some fields, read the current record first, merge your changes, and send the result back.
Live updates (optional)
To make a dashboard update itself as submissions arrive, subscribe to the live-sync stream and listen for collection-record events. Open the stream only after a successful owner load, so a visitor who lands on the page never tries to connect.
const es = new EventSource('/_/live-sync/stream?page-url=' + encodeURIComponent(location.href))
es.addEventListener('collection-record', (e) => {
const { op, id, data, modifiedAt } = JSON.parse(e.data)
// op is 'create', 'update', or 'delete'. Update your list in place.
})Pausing and unmarking
The folder menu also lets you pause submissions (the form starts returning a “not accepting” error) and unmark the collection (the form and dashboard stop working, but your files and records are kept, and you can re-mark anytime).
If you hand out a form link, remember that the link is just the page’s file URL. Renaming or moving the file, or unmarking the collection, breaks that link. The Collection ID itself never changes, but the page URL is whatever you have named the file.
Working with the data elsewhere
Records are ordinary JSON files in your records/ folder, so anything else you build on Hyperclay can read them, and they sync to the desktop app like the rest of your files. The extractData and applyData helpers from the JSON API are a convenient way to move values between a form and a record object, but they are optional. A record is just JSON you control.