Developers
Build an app for the Hespera editor
An app is a small page that runs beside someone's work. It receives only what they allow, hands back ordinary objects, and disappears. Everything below is enforced — by the browser, by the interface, or by a person — not promised.
The one idea everything else follows from
Your app produces content. It never draws frames. It asks for what it is allowed to see, computes on its own, and returns plain objects — rectangles, text, an image. Our renderer draws them.
- At export time your code does not run at all. The server draws an ordinary object, so what someone sees on the canvas is exactly what they download.
- A broken or unreachable app cannot spoil finished work — it already handed its result over and is out of the picture.
- The interface has a finite list of things to ask for, and each one can be checked and refused.
Where your code runs
In an iframe with sandbox="allow-scripts" and deliberately without allow-same-origin. That gives your page an opaque origin of its own: no access to the page around it, to the session, to storage, to cookies. Not by our discipline — by the browser's refusal.
We serve your page with a Content-Security-Policy built from your manifest, and the sandbox is repeated in that header — losing the attribute on the frame would not quietly hand your code our origin.
Your page is served from sandbox.hesperaai.com — a domain that holds nothing else: no accounts, no mail, no pages of ours. Even if the sandbox were ever lost, your code would still not be sitting on the origin where people are signed in.
⚠️ A sandbox without same-origin means your page has no origin of its own, so requests from it arrive at your server as Origin: null — not as the domain above. Do not build access control on that header: allow null in your CORS setup and decide who is calling from app.token instead. That is what the token is for.
Your app may have a backend — declare where it goes
List the exact origins you call in hosts, and say in one sentence why — hosts_reason is required, not decoration. That sentence is shown to the person beside your name — in the editor before your app first runs, and in their cabinet settings. The browser blocks every origin you did not list; wildcards are refused at submission, because a wildcard is permission to go anywhere wearing the shape of a restriction.
Declaring nothing is a perfectly good answer, and the better one when it is true: the app then shows as "does not go online", and the browser will not let anything out no matter what the code tries. All six of our own apps are like that.
Your backend can tell a real call from a forged one
Ask us for a token — call("app.token") — and send it to your server. It is a five-minute JWT signed with your app's key (you find the key in your partner cabinet, and you can replace it there if it leaks). Verify the signature and you know the request came from a genuine run of your app.
Inside it: app, ver, tid and uid — the cabinet and the person, as identifiers specific to your app. The same person looks different to a different app, so nobody can be matched across the shelf. Replacing your key changes those identifiers too: to your server it will look like a new set of users.
Without a declared host the token is refused — there would be nowhere to send it.
What you can ask for
| Call | Needs | Gives you |
|---|---|---|
page.info | — | page size and cabinet language |
selection.info | selection.read | type and size of what is selected |
image.get | image.read | the selected photo as data, 1024px on the long side |
canvas.addRects | canvas.add | up to 64 rectangles per call |
canvas.addText | canvas.add | one line of text, up to 400 characters |
canvas.addLines | canvas.add | up to 400 segments as ONE vector object |
canvas.addImage | canvas.add | your picture on the page |
canvas.replaceImage | canvas.replace | replaces the selected photo |
brand.get | brand.read | the cabinet's colours and fonts |
media.save | media.write | saves a file to their Uploads |
ui.notify | — | a short line in the editor's own strip |
canvas.remove | canvas.add | removes something you added this run — nothing else |
app.token | — | a five-minute signed token for your own backend |
There is no way to subscribe to events, read the document, or reach another app. The shorter the list, the fewer ways there are to use it wrong.
Settings that change something should change it live. Add your object, keep the id you get back, and on the next change remove the old one and add the new — that is what canvas.remove is for. It refuses anything you did not add yourself in this run, so it is not power over the page, only over your own result. Update on release rather than on every pixel of a slider: each change costs two calls, and the interface counts them.
Permissions
Three at most, and not as a guideline — as a limit. People read the consent screen, not a registry. A fourth line means the permissions are cut finer than anyone can weigh, and then the single line that matters (your photo will be sent to example.com) gets skimmed along with the rest.
We ask the person only when two things coincide: your app reads photos and it has a declared host. An app that reads but cannot send takes nothing — the browser will not let the data out. Asking permission for what is physically impossible is a ritual, and rituals devalue the one case that matters.
Limits
- 8 MB — one image from your app
- 64 objects per call, 400 per run
- 120 calls per minute
- 400 characters of text, 140 in a notification
- 12 saves to Uploads per run
- 512 KB — your page itself
Limits are not decoration. An app can be badly written rather than ill-meant: a loop that puts an object on the page sixty times a second freezes someone's tab just as thoroughly as malice. We count both the same way.
How to send it
- Write one HTML file.
call(),bitmap(),canvasOf(),$()andLOCALEare injected by us — do not ship your own. - Open your partner cabinet → Apps, fill in the manifest, attach the file.
- Once approved it is simply there for everyone — there is nothing to install. A paid app charges its price the first time a cabinet opens it, once, through our checkout; after that it just opens.
- Choose who gets it. My team only works immediately and we do not review it — the risk stays inside your team. Everyone goes into review.
We take the file, never a link. Checking a link once is meaningless: tomorrow it serves different code while our approval still stands. We fingerprint what we stored, and the version row is append-only — approval covers a fingerprint, not an app forever. A new version is reviewed again.
Refused at submission — not flagged, refused
These are not weighed by a reviewer. The submission simply does not go through, and the answer names what was found.
- A password or card field. No editor app needs one for anything. Inside the frame your pixels sit within someone's cabinet and borrow the trust they have in us — a form that looks like ours is indistinguishable from ours.
- Taking payment. A paid app is paid through our checkout: name a price in credits and 70% comes to you, the same share as for templates. Your own checkout inside the frame is not another way to be paid — it is a way around both our obligations and ours to the person.
- Navigating the frame to another page, or a form that posts outward.
- A wildcard host, a host over plain http, or hosts declared without a reason.
Also refused, after a person reads it
- A manifest that asks for more than the app plainly does.
- Code we cannot read: minified, obfuscated, or fetched at run time.
- Pretending to be part of Hespera — your app always shows as third-party.
And the honest part, stated here rather than hidden in a policy: an app that was handed a photo has seen that photo — inside the browser, for as long as it is open. No sandbox undoes that. What the no-network rule does is make that the end of the story: seen, used, and gone when the panel closes.
Take the pieces
example.html — a complete working app
hespera-apps.d.ts — types for all thirteen calls
The agreement
Publishing is governed by the App Developer Terms, which you accept by submitting. Worth reading before you write code rather than after: they set out what review can refuse, when we may switch an app off for everyone, what happens to data your app receives, and how the 70% share is paid.