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 | |
|---|---|
| App | Your service: an identity, and the capabilities it implements |
| Capability | An interface the platform calls — git, runtime. A group of operations, some required, some optional |
| Installation | Your app connected to an organization, by one of its owners or admins. Whatever credential that produces is yours to keep |
| Grant | A 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.
| Endpoint | Called by | Signed | Purpose |
|---|---|---|---|
GET /manifest | Spunto, server to server | No | What the app is and implements |
GET {installPath}?launch=… | A member's browser | Launch token | Start connecting the app to an organization |
GET {grantPath}?launch=… | A member's browser | Launch token | Start a member's personal authorization (optional) |
POST /oauth/complete | Spunto | Yes | Commit an installation or a grant Spunto confirms |
POST /status | Spunto | Yes | What this organization has connected |
POST /capabilities/{capability}/{operation} | Spunto | Yes | Do 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 | |
|---|---|
id | Stable identifier. Also your client_id for Connect with Spunto |
capabilities | One entry per capability, listing the operations you actually implement |
userGrantOperations | Operations that need a member's grant on top of the installation. Omit if none |
auth | oauth, apiToken (a form where the consent screen would be) or none |
accent | Your card's colour, as a CSS colour. Apps get a generic icon for now: Spunto shows no image an app serves |
publicUrl | Where a browser reaches the app. installPath, grantPath and managePath are paths under it |
managePath | Optional: the app's own settings page for an organization that installed it — shown as Manage ↗ |
oauth.redirectUris | Optional: 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.
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.
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.
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.
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.
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:
| Code | Means |
|---|---|
not_installed | This organization has not connected the app — an owner or admin has to |
grant_required | The 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.
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.
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.
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.
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, accent | As reviewed |
publicUrl, installPath, grantPath, managePath | As reviewed |
| Base URL | As reviewed |
capabilities | Only those approved; their operations as the live manifest says |
oauth.redirectUris | Only 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
gitandruntimecapabilities are open to apps for now. - Your pages live on your own origin:
publicUrlis an absolutehttpsURL, 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.
Git hosting — git
Where a project's repositories live: clone credentials, repository and branch pickers, pushes as the member, pull request status.
Sandbox runtime — runtime
Where sandboxes run: create, start, stop and destroy a box that runs Spunto's agent.
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.