Documentation
AlphaInstall flow and receipts
Send people to install your app, bring them back to your site, and prove the redirect came from Upstream.
The Upstream API is in alpha. Endpoints, fields and limits can change before it's stable. Send feedback to support@coline.app.
People install an app from its install page. Afterward, Upstream can send them back to your site with the installation's ID and a signed receipt, so you can tie the installation to their account on your side.
Install link
https://upstream.codes/apps/{slug}/installations/new?state={state}
state is any string up to 1,024 characters. Upstream hands it back unchanged. Use it to tie the install to a session on your site, and check it when the person comes back.
The person picks one of the namespaces they own and the repositories the app can use, then approves. In Coline, the same page is https://coline.app/{workspace}/upstream/apps/{slug}/install?state={state}.
Install without a browser
A namespace owner, or an agent signed in as one, can install without the page:
upstream app install my-bot --owner acme --repo api
This needs CLI 0.8.1 or newer. An older one says unknown flag: --owner; run upstream update.
The CLI calls POST /v1/user/installations with the person's own token, which needs apps:write. upstream auth login grants it. The result is the same as installing on the page: the same owner, SSO and private-app checks, the app's current permissions, the installation webhook and an entry in the audit log. Installation tokens, app JWTs and tokens limited to some repositories can't install apps.
There's no browser to redirect, so a setup URL isn't visited. When the app has one, the response's setup.url is the URL the browser would have opened, with the same installation_id, setup_action, state and receipt, and setup.receipt is the receipt alone. Open the URL or hand the receipt to the app; it checks it as usual. An app that asks people to sign in when they install gets no OAuth code this way, since signing in needs the person's browser. setup is null without a setup URL, and on updates unless Redirect on update is on.
Pending permissions are approved only when asked: --approve in the CLI, approvePermissions: true in the API.
Setup URL
Set a Setup URL under Installation in your app's settings. After someone installs the app, Upstream sends them there with these query parameters:
| Parameter | Value |
|---|---|
installation_id | The installation's numeric ID, as the GitHub-compatible API uses |
setup_action | install or update |
state | The state from the install link, when there was one |
receipt | A JWT signed by Upstream. It lasts 5 minutes. |
https://example.com/upstream/setup?installation_id=81236571&setup_action=install&state=c2Vzc2lvbi0xMjM&receipt=eyJhbGciOiJFZERTQSIs...
Setup URLs use HTTPS, or plain HTTP on localhost, 127.0.0.1 or [::1]. They can't contain a user name, a password or a fragment.
Turn on Redirect on update to also send people there after they change the app's repositories or approve new permissions. Those redirects carry setup_action=update. Without a setup URL, people stay on Upstream.
The install page tells people where they'll go next, such as "You'll go to example.com next."
Ask people to sign in when they install
Turn on Ask people to sign in when they install to learn who installed the app as well as where. People then go to your first callback URL instead of the setup URL. The redirect carries the same parameters plus an OAuth code.
Exchange the code like a sign-in code, with your first callback URL as redirect_uri:
curl -X POST https://upstream.codes/login/oauth/access_token \
-H "Accept: application/json" \
-d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET \
-d code=$CODE -d redirect_uri=https://example.com/callback
The token acts as the person who installed the app, with the app's permissions. The code works once, within 10 minutes. No PKCE verifier is needed. This needs a client secret, so only server apps can turn it on. A public client gets 400 OAUTH_ON_INSTALL_NEEDS_SECRET.
Check the receipt
The query parameters come through the person's browser, so anyone can type them. Check the receipt before you trust installation_id. There are two ways to do it.
Offline, with Upstream's public keys
The receipt is an EdDSA JWT. Its header has typ: upstream-install-receipt+jwt and a kid from the key set at https://upstream.codes/.well-known/jwks.json.
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://upstream.codes/.well-known/jwks.json"));
// Keep used receipt IDs until they expire. Use shared storage if you run more than one server.
const usedReceipts = new Set<string>();
export async function checkReceipt(receipt: string, expectedState: string) {
const { payload } = await jwtVerify(receipt, jwks, {
issuer: "https://upstream.codes",
audience: "urn:upstream:app:my-bot",
typ: "upstream-install-receipt+jwt",
algorithms: ["EdDSA"],
});
if (payload.state !== expectedState) throw new Error("The state doesn't match this session.");
if (!payload.jti || usedReceipts.has(payload.jti)) throw new Error("This receipt was already used.");
usedReceipts.add(payload.jti);
return payload;
}
| Claim | Value |
|---|---|
iss | https://upstream.codes |
aud | urn:upstream:app:{slug} |
sub | The installation's UUID, as the native API uses |
jti | This receipt's unique ID. Keep it until exp and refuse it if it comes back. |
iat, exp | When Upstream signed it, and 5 minutes later |
app_id, app_slug | Your app's UUID and slug |
installation_id | The installation's numeric ID |
setup_action | install or update |
state | The state from the install link. Missing when there was none. |
account | { login, type } for the namespace the app is installed on. type is User or Organization. |
repository_selection | all or selected |
sender | { id, login } for the person who installed it. id is their Upstream user ID. |
On your server, once
POST /app/install-receipts/verify checks the receipt and marks it used, so you don't keep your own list of jti values. Call it with your app JWT. With Octokit's App:
const { data } = await app.octokit.request("POST /app/install-receipts/verify", {
receipt: String(request.query.receipt),
});
// data.installation, data.setup_action, data.state, data.account
With curl, in the native dialect:
curl -X POST https://api.upstream.codes/v1/app/install-receipts/verify \
-H "Authorization: Bearer $APP_JWT" \
-H "Content-Type: application/json" \
-d '{"receipt":"'"$RECEIPT"'"}'
{
"data": {
"installation": { "id": "5d0c1f7e-…", "repositorySelection": "selected", "permissions": ["repo:read", "issues:write"] },
"setupAction": "install",
"state": "c2Vzc2lvbi0xMjM",
"account": { "login": "acme", "type": "Organization" }
}
}
The GitHub-compatible path, /api/v3/app/install-receipts/verify, answers with installation as GitHub shapes it, setup_action, state and account.
| Status | Code | Means |
|---|---|---|
409 | RECEIPT_USED | This receipt was already verified. |
400 | RECEIPT_INVALID | The signature is wrong, the receipt is for another app, or it expired. |
404 | INSTALLATION_NOT_FOUND | The installation was removed, or your app is suspended. |
Installation events
Every install, removal and repository change also sends your app an installation or installation_repositories webhook. See App webhooks. The setup URL is for the person's browser. The webhook is the record your server can rely on.