Documentation

Alpha

App manifests

Create a configured app from a JSON manifest in a few clicks, with GitHub's manifest flow.

The Upstream API is in alpha. Endpoints, fields and limits can change before it's stable. Send feedback to support@coline.app.

A manifest describes an app in JSON. You post it to Upstream, the person reviews it and creates the app, and your server trades a one-time code for the app's credentials. Tools use this to set up an app for someone without making them copy settings by hand. upstream app create uses it too.

To create an app from a manifest with your own token and no browser, for example from an agent or a server, send it to POST /v1/apps instead.

1. Post the manifest

Put a form on your site that posts a manifest field to /settings/apps/new:

<form action="https://upstream.codes/settings/apps/new?state=abc123" method="post">
  <input type="hidden" name="manifest" id="manifest">
  <button type="submit">Create an Upstream app</button>
</form>
<script>
  document.getElementById("manifest").value = JSON.stringify({
    name: "Acme CI",
    url: "https://ci.acme.dev",
    description: "Runs Acme CI on every push.",
    public: true,
    redirect_url: "https://ci.acme.dev/upstream/manifest",
    callback_urls: ["https://ci.acme.dev/upstream/oauth/callback"],
    setup_url: "https://ci.acme.dev/upstream/setup",
    hook_attributes: { url: "https://ci.acme.dev/upstream/webhook" },
    default_permissions: { contents: "read", checks: "write", pull_requests: "read" },
    default_events: ["check_suite", "check_run"],
  });
</script>

Upstream shows a review page. The person signs in if needed, picks the owner, can change the name, and chooses Create app. Nothing is created before that. Manifests can be up to 8 KB, and state up to 1,024 characters.

You can also link straight to the review page with GET /settings/apps/new?manifest={url-encoded JSON}&state={state}. Add &owner={namespace} to pick the owner for them.

2. Get the code

Upstream sends the person to redirect_url with a code and your state:

https://ci.acme.dev/upstream/manifest?code=umc_…&state=abc123

Check that state is the one you sent.

3. Trade the code for credentials

curl -X POST https://api.upstream.codes/api/v3/app-manifests/$CODE/conversions

This needs no authentication. The code works once, within an hour. The response is the app in GitHub's shape, with its secrets:

{
  "id": 81236571,
  "slug": "acme-ci",
  "node_id": "…",
  "name": "Acme CI",
  "description": "Runs Acme CI on every push.",
  "owner": { "login": "acme", "type": "Organization" },
  "external_url": "https://ci.acme.dev",
  "html_url": "https://upstream.codes/apps/acme-ci",
  "created_at": "2026-11-09T18:00:00.000Z",
  "updated_at": "2026-11-09T18:00:00.000Z",
  "client_id": "uoa_…",
  "client_secret": "uoc_…",
  "webhook_secret": "uhs_…",
  "pem": "-----BEGIN PRIVATE KEY-----\n…",
  "permissions": { "contents": "read", "pull_requests": "read", "checks": "write" },
  "events": ["check_suite", "check_run"]
}

client_secret is null for public clients. Save pem, webhook_secret and client_secret right away. Upstream can't show them again.

The native path, POST /v1/app-manifests/{code}/conversions, returns id as a UUID and camelCase fields: slug, name, clientId, clientSecret, webhookSecret, pem, permissions as Upstream scopes, events and htmlUrl.

Without redirect_url, the person sees the credentials once after creating the app, and the private key downloads.

Fields

These are GitHub's manifest fields:

FieldTypeNotes
namestringRequired. Up to 120 characters. The slug comes from it.
urlstringHomepage
descriptionstring
publicbooleanAnyone can install it. Defaults to false.
redirect_urlstringWhere the code goes. HTTPS, or HTTP on loopback.
callback_urlsstring[]OAuth callback URLs, up to 20. callback_url works too. Defaults to redirect_url.
setup_urlstringSee Setup URL
setup_on_updatebooleanRedirect on update
request_oauth_on_installbooleanAsk people to sign in when they install. Needs a confidential client.
hook_attributes{ url, active }Webhook URL. active defaults to true.
default_permissionsobjectGitHub permission names and levels. See the table below.
default_eventsstring[]Webhook events

Upstream also reads these:

FieldTypeNotes
webhook_format"github" or "native"Payload format. Defaults to github. With native, default_events take Upstream event names.
client_type"confidential" or "public"Public clients have no client secret and sign people in with PKCE. Defaults to confidential.
device_flow_enabledbooleanAllows the device flow. Defaults to true for public clients.
scopesstring[]Upstream scope names, added to default_permissions

A manifest with no permissions gets repo:read. A bad field returns MANIFEST_INVALID with a message that names it, such as default_permissions.wiki: not a supported permission.

Permissions

GitHub permissionreadwrite
contentsrepo:readcontents:write
metadatarepo:read
issuesissues:readissues:write
pull_requestschanges:readchanges:write
checks, statusesrepo:readchecks:write
actionsactions:readactions:write
administrationrepo:readrepo:admin
repository_hookswebhooks:adminwebhooks:admin
discussionsrepo:readdiscussions:write
members, organization_administrationorg:readorg:admin
email_addressesuser:read
profileuser:readuser:write
notificationsnotifications:read
provenanceprovenance:read

admin counts as write. A level with no scope in the table is refused. contents:write writes files, Git objects, branches and tags, without repo:write's right to create repositories. discussions:write creates and edits repository discussions.