Embedding your app's interface
An app can render its own page inside Stoneity. The page is your own web app, loaded in an iframe, with a short-lived signed token that tells you who is looking and at what.
Declaring a surface
Add surfaces to the app manifest. Each entry places one page at one location:
{
"key": "acme-crm",
"egress": ["app.acme.com"],
"surfaces": [
{ "location": "ticket_sidebar", "url": "https://app.acme.com/stoneity/ticket", "title": { "en": "Acme CRM", "tr": "Acme CRM" }, "height": 460 },
{ "location": "admin_page", "url": "https://app.acme.com/stoneity/admin", "title": { "en": "Acme settings" } }
]
}| Location | Where it appears |
|---|---|
ticket_sidebar | A tab on the ticket, next to History and Links |
ticket_toolbar | A button on the ticket that opens the page in a dialog |
admin_page | Open app on the installation row under Settings → Apps |
Rules enforced when the app is installed:
- The URL must be
https. - Its host must appear in the app's
egresslist. That list is what the workspace admin is shown as "this app sends your data to", so the frame cannot go anywhere else. heightis the starting height, between 120 and 1200. Your page can change it at runtime.- A location can be declared only once per app.
The token
Stoneity appends ?token= to your URL. The token is a JWT signed with HS256 using a key derived from your installation's shared secret:
key = HMAC-SHA256(sharedSecret, "stoneity-surface-token-v1")The shared secret is the one shown as part of the installation's inbound address. Because the key is per installation, one workspace's token can never validate in another.
{
"iss": "stoneity",
"aud": "acme-crm",
"sub": "42",
"exp": 1789000000,
"installation": 7,
"surface": "ticket_sidebar",
"workspace": "https://acme.stoneity.com",
"user": { "id": 42, "name": "Ada Lovelace", "email": "ada@acme.com", "language": "tr" },
"ticket": { "id": "651f…", "boardId": "b1" }
}Always verify the signature and exp before trusting any of it. The token lives for five minutes; Stoneity reloads the frame with a fresh one before it expires. Treat it as proof of this one view, not as a session.
To act on the workspace's data, use the Public API with an API key. The surface token identifies the viewer; it is not an API credential.
Talking to Stoneity
Your page can call a small set of commands with postMessage. Send them to window.parent, targeting the workspace origin from the workspace claim.
parent.postMessage({ type: 'stoneity:ready' }, workspaceOrigin);
window.addEventListener('message', (event) => {
if (event.data?.type === 'stoneity:context') {
// { user, ticket, installationId, location }
}
});
parent.postMessage({ type: 'stoneity:resize', height: document.body.scrollHeight }, workspaceOrigin);
parent.postMessage({ type: 'stoneity:showToast', level: 'success', message: 'Saved' }, workspaceOrigin);
parent.postMessage({ type: 'stoneity:close' }, workspaceOrigin);| Command | Effect |
|---|---|
stoneity:ready / stoneity:getContext | Stoneity replies with stoneity:context |
stoneity:resize | Sets the frame height, clamped to 120–1200 |
stoneity:showToast | Shows a notification, first 300 characters, as plain text |
stoneity:navigate | Navigates the workspace to a relative path such as /agent/home |
stoneity:close | Closes the dialog, where the surface is shown in one |
Anything else is ignored. Stoneity checks both the sender's origin and the frame it came from, so only your page can drive these.
Sandbox
The frame runs with allow-scripts allow-forms allow-popups allow-same-origin. Notably absent is allow-top-navigation: your page cannot move the workspace to another address. Use stoneity:navigate for in-app navigation instead.