Building an app

The contract between Spunto and an app — manifest, installation, signed capability calls and status. The same one GitHub and Google Cloud use.

An app is an HTTP service you run, on your own domain, with your own database. Spunto knows three things about it: a base URL, a signing secret Spunto gives you when you register it, and the manifest the app serves. Everything else — the tokens it obtains, the accounts it connects, the machines it runs — stays with the app.

This is not a simplified version of how first-party integrations work. GitHub (git), Google Cloud (runtime) and the machines-you-bring runtime are separate services that speak exactly this contract; nothing on this page is reserved to them.

What it is
AppYour service: an identity, and the capabilities it implements
CapabilityAn interface the platform calls — git, runtime. A group of operations, some required, some optional
InstallationYour app connected to an organization, by one of its owners or admins. Whatever credential that produces is yours to keep
GrantA member's personal authorization, under an installation — only for what cannot be done as the organization (registering a member's SSH key on their own account, pushing as them)

Installations belong to organizations on purpose: what an app produces — a clone credential, a machine — belongs to the organization, and must keep working when the person who connected it leaves. A grant is the exception, not half the model.

EndpointCalled bySignedPurpose
GET /manifestSpunto, server to serverNoWhat the app is and implements
GET {installPath}?launch=…A member's browserLaunch tokenStart connecting the app to an organization
GET {grantPath}?launch=…A member's browserLaunch tokenStart a member's personal authorization (optional)
POST /oauth/completeSpuntoYesCommit an installation or a grant Spunto confirms
POST /statusSpuntoYesWhat this organization has connected
POST /capabilities/{capability}/{operation}SpuntoYesDo the work

Signed calls carry an HMAC over the method, the path and the body → Signed requests.

GET /manifest
{
  "id": "acme-git",
  "name": "Acme Git",
  "description": "Repositories hosted on Acme Git.",
  "publisher": "Acme",
  "accent": "#2563eb",
  "auth": "oauth",
  "capabilities": [
    {
      "capability": "git",
      "operations": ["cloneCredential", "listRepos", "listBranches", "userCredential"]
    }
  ],
  "userGrantOperations": ["userCredential"],
  "publicUrl": "https://git.acme.dev/spunto",
  "installPath": "/oauth/install",
  "grantPath": "/oauth/user",
  "managePath": "/settings"
}
Field
idStable identifier. Also your client_id for Connect with Spunto
capabilitiesOne entry per capability, listing the operations you actually implement
userGrantOperationsOperations that need a member's grant on top of the installation. Omit if none
authoauth, apiToken (a form where the consent screen would be) or none
accentYour card's colour, as a CSS colour. Apps get a generic icon for now: Spunto shows no image an app serves
publicUrlWhere a browser reaches the app. installPath, grantPath and managePath are paths under it
managePathOptional: the app's own settings page for an organization that installed it — shown as Manage ↗
oauth.redirectUrisOptional: where Connect with Spunto may send a user back

Spunto re-reads the manifest every minute. Adding an operation to your app is a deploy of your app — no release of Spunto, and the buttons that depend on it appear on their own.

A manifest is accepted whole or not at all

An app that claims a capability without one of its required operations, or names a capability that does not exist, is rejected entirely, not trimmed. Accepting half of it would turn a typo into an app that looks installed and fails the first time a sandbox needs it.

The dashboard shows a feature if and only if an installed app implements the operation behind it. A git host without registerUserSshKey still clones — members simply aren't offered to register their key with it. A runtime without pause still runs sandboxes — the Pause button is simply not there. Declare what you implement, nothing more, and the product adapts.

A browser redirect authenticates nobody. So the parameters of an installation are never taken from a query string: Spunto signs them, and confirms the result server to server.

1

Spunto launches the flow

An owner or admin clicks Connect on the Integrations page. Their browser goes to {publicUrl}{installPath}?launch=<token> — a launch token signed with your secret, saying which organization, which user, where to come back, and that it expires in ten minutes.

2

Your app does its part

Verify the token. Then do whatever connecting means for you: an OAuth dance with your own provider, or a form asking for an API key. Spunto is not involved and sees none of it.

3

Stage the result, send the browser back

Store the result as pending, keyed by an opaque id, for a few minutes. Redirect to the token's return URL with ?completion=<id> appended. Do not commit it yet.

4

Spunto confirms

Spunto calls POST /oauth/complete with the organization, the user who is actually signed in, the completion id, and whether that user may install apps (canInstall, checked again now). Commit only if the organization and user match what you staged, and — for an installation — canInstall is true.

// POST /oauth/complete — signed
{ "orgId": "org_…", "userId": "usr_…", "completion": "c8f1…", "canInstall": true }
 
// 200
{ "result": { "kind": "install" } }   // or "grant"

The confirmation step is what makes a forwarded link harmless: an attacker who sends their launch link to someone else cannot attach that person's account to the attacker's organization, because the user Spunto confirms with is whoever is really signed in.

A grant follows the same four steps, starting from grantPath with a launch token whose purpose is grant, and committing for that one member.

Spunto holds no copy of what an organization connected. To draw the Integrations page it asks:

// POST /status — signed
{ "orgId": "org_…", "userId": "usr_…" }
 
// 200
{
  "result": {
    "installations": [
      {
        "externalId": "48213",
        "accountLogin": "acme",
        "accountType": "Organization",
        "accountAvatarUrl": "https://…",
        "installedAt": "2026-10-02T09:14:00Z"
      }
    ],
    "grant": { "present": false, "externalId": null }
  }
}

Always return grant, even if your app has no grants — { "present": false, "externalId": null }. Disconnecting happens in your app (on your managePath, say); the next /status is how Spunto finds out.

Every operation is one signed POST, with one envelope:

// POST /capabilities/git/cloneCredential — signed
{
  "orgId": "org_…",
  "userId": "usr_…",
  "input": { "repo": "acme/api", "repos": ["acme/web"] }
}

orgId says whose installation to use. userId is present when a person is behind the call, and is the one to act as for a grant operation. input is the operation's own — see each capability's reference.

Answer with { "result": … }:

{ "result": { "scheme": "https-token", "username": "x-access-token", "token": "…", "expiresInSeconds": 3600 } }

Any non-2xx with a JSON body { "error": "…", "code": "…" }. The message is shown to the member. Two codes are kept apart all the way to the dashboard, because they ask the member for different things:

CodeMeans
not_installedThis organization has not connected the app — an owner or admin has to
grant_requiredThe operation needs this member's own authorization — they have to grant it

Our apps answer both with 409; the code is what counts. Anything else is reported as a failure of your app.

Spunto waits 10 seconds for an answer. The call itself is never retried: one implementation answers, the result matters, and replaying a request blindly is how two of something get created. Where a capability does retry — runtime.create, when the app was unreachable or timed out — it says so, and the operation must be idempotent on the id it is given.

Long work (booting a machine, building an image) does not fit in 10 seconds and is not meant to: answer at once with a handle, and report progress through the status operation the capability defines.

Spunto calls your app on its base URL — which can be private, a name on an internal network. Browsers reach it on publicUrl. They are often the same; they don't have to be.

1

Register it

In Settings → Developer apps, an owner or admin gives the app an id — the one its manifest declares, lower-case letters, digits and dashes — and its base URL, in https. Spunto shows the signing secret once: put it in your app's configuration.

2

Try it in your organization

Check reads your manifest the way the catalog does and makes a signed POST /status, and says what is wrong if anything is. Once it passes, the app appears on your organization's Integrations page, marked Your app · not reviewed: install it and use it for real. Nobody else can see it, call it, or sign in with it.

3

Submit it for review

Submit for review with a note for the reviewer: what the app does, how to try it. Spunto snapshots your manifest at that moment — that version is the one reviewed.

4

Listed

Once approved, the app is offered to every organization, marked Marketplace. If it isn't, the reason is shown on your app; fix it and submit again.

The manifest is still read live, so a listed app can gain operations within the capabilities it was approved for, without anyone reviewing them again. Everything else stays as reviewed until a new version is approved:

For other organizations
name, description, publisher, icon, accentAs reviewed
publicUrl, installPath, grantPath, managePathAs reviewed
Base URLAs reviewed
capabilitiesOnly those approved; their operations as the live manifest says
oauth.redirectUrisOnly those approved

To change any of them, Submit an update: the listed version stays in place while the new one is reviewed. To try a change before submitting it, register a second app — acme-git-staging — and keep the listed one for what was reviewed.

  • Only the git and runtime capabilities are open to apps for now.
  • Your pages live on your own origin: publicUrl is an absolute https URL, or left out to use the base URL.
  • Spunto only calls public addresses, and doesn't follow redirects.
  • Unlist takes a listed app off the marketplace: organizations using it lose it at once. Spunto may also suspend an app, which removes it from every organization and refuses every call to it.

The catalog of capabilities is the platform's and is closed: an app cannot invent one, since nothing would ever call it. Agent harnesses are apps too, but run inside the platform — the stream of a session is too hot a path to proxy — so they are not open to third parties.