Skip to content

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:

json
{
  "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" } }
  ]
}
LocationWhere it appears
ticket_sidebarA tab on the ticket, next to History and Links
ticket_toolbarA button on the ticket that opens the page in a dialog
admin_pageOpen 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 egress list. That list is what the workspace admin is shown as "this app sends your data to", so the frame cannot go anywhere else.
  • height is 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.

json
{
  "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.

js
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);
CommandEffect
stoneity:ready / stoneity:getContextStoneity replies with stoneity:context
stoneity:resizeSets the frame height, clamped to 120–1200
stoneity:showToastShows a notification, first 300 characters, as plain text
stoneity:navigateNavigates the workspace to a relative path such as /agent/home
stoneity:closeCloses 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.

Stoneity Public API v1