Connect with Spunto
Sign people into your app with their Spunto account — OAuth 2 authorization code with PKCE, and who they are in which organizations.
Spunto is an OAuth 2 provider for its apps. "Connect with Spunto" tells your app who a member is, which organizations they belong to, and with which role — enough to show each member what belongs to their organization, and to let only owners and admins change it.
It is what the page where an organization manages its own machines uses: that page lives on the app's domain, not in the dashboard, and needs to know who is looking at it.
| Flow | Authorization code, PKCE S256 required |
client_id | Your app's id, from its manifest |
client_secret | Your app's signing secret — the one secret you and Spunto already share |
| Redirect URIs | Those your manifest declares in oauth.redirectUris, compared exactly. HTTPS only (http is accepted for localhost). Once listed, only those that were reviewed |
| Who can sign in | Until your app is listed, only members of the organization that registered it |
| Scopes | identity |
| Access token | Opaque (spo_…), valid 1 hour. No refresh token |
Send the user to Spunto
Generate a code_verifier and a state, keep both (a short-lived signed cookie does), and redirect:
https://spunto.net/api/oauth/authorize
?response_type=code
&client_id=acme-git
&redirect_uri=https%3A%2F%2Fgit.acme.dev%2Fspunto%2Foauth%2Fcallback
&scope=identity
&state=…
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256A user who isn't signed in to Spunto signs in first. The first time, they see a consent screen; after that, the consent is remembered for your app and these scopes, and they come straight back.
Get the code back
Spunto redirects to your redirect_uri with ?code=…&state=…. Check state. The code is valid for 60 seconds and works once.
Exchange it, server to server
curl -X POST https://spunto.net/api/oauth/token \
-d grant_type=authorization_code \
-d code=… \
-d redirect_uri=https://git.acme.dev/spunto/oauth/callback \
-d code_verifier=… \
-d client_id=acme-git \
-d client_secret=…Form-encoded or JSON; the client credentials may also go in an HTTP Basic header.
{ "access_token": "spo_…", "token_type": "Bearer", "expires_in": 3600, "scope": "identity" }Ask who it is
curl https://spunto.net/api/oauth/userinfo -H "Authorization: Bearer spo_…"{
"sub": "usr_…",
"name": "Ada Lovelace",
"email": "ada@acme.dev",
"picture": "https://…",
"orgs": [{ "id": "org_…", "name": "Acme", "slug": "acme", "role": "admin" }]
}Then open your own session — a cookie on your domain — with what userinfo said. When it expires, run the flow again: with the consent remembered, it costs the user no click.
- Errors on an unknown redirect URI are not redirected. If the
redirect_uriis not one your manifest declares, Spunto shows the error itself rather than sending the browser to an address it can't vouch for. - A role read at sign-in is a role at sign-in. If an admin is demoted, your app learns it at their next connection. Keep your sessions short if that matters to you.
org.idis the same id Spunto sends in every capability call — it is how you match a signed-in member to the installations of their organization.- Serve your pages on your own origin. OAuth only means something between separate origins; never mount your app's pages under Spunto's domain.
- An API key can't authorize. Only a member signed in to the dashboard can consent; an
spk_key is refused.