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.

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 whyhosts_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

CallNeedsGives you
page.infopage size and cabinet language
selection.infoselection.readtype and size of what is selected
image.getimage.readthe selected photo as data, 1024px on the long side
canvas.addRectscanvas.addup to 64 rectangles per call
canvas.addTextcanvas.addone line of text, up to 400 characters
canvas.addLinescanvas.addup to 400 segments as ONE vector object
canvas.addImagecanvas.addyour picture on the page
canvas.replaceImagecanvas.replacereplaces the selected photo
brand.getbrand.readthe cabinet's colours and fonts
media.savemedia.writesaves a file to their Uploads
ui.notifya short line in the editor's own strip
canvas.removecanvas.addremoves something you added this run — nothing else
app.tokena 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

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

  1. Write one HTML file. call(), bitmap(), canvasOf(), $() and LOCALE are injected by us — do not ship your own.
  2. Open your partner cabinet → Apps, fill in the manifest, attach the file.
  3. 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.
  4. 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.

Also refused, after a person reads it

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.

hespera.ai · developers@hespera.ai