Create an app

To build an app for Stok.ly, you first register it in the developer portal. Creating an app gives you the credentials your app needs, a Client ID, a Client Secret and a Webhook Secret, and is where you declare what your app does, via a manifest, before anyone can install it into their account.

🚧

The developer portal is still under active development. This page describes the intended workflow, specifics that aren't finalized yet are flagged inline below.

Before you start

You'll need a developer account, and your developer account will need to accept Stok.ly's developer agreement before you can create an app, and before you publish a release.

Creating your app

From the developer portal, an app has these details:

FieldRequiredDescription
NameyesShown to users on your app's marketplace listing. Up to 128 characters.
DescriptionyesA short summary shown to users considering installing your app. Up to 255 characters.
Support emailyesWhere users can contact you about your app.
IconnoShown alongside your app's name.
Long descriptionnoThe full description on your listing. Up to 8000 characters.
ScreenshotsnoImages shown on your listing.
Privacy policy URLnoLinked from your listing.
Homepage URLnoLinked from your listing.

Uploading your manifest

Each release of your app is defined by a manifest.json file. It describes what your app needs and does: the scopes it requests, where Stok.ly should redirect users after they authorize your app, the pages it adds, and the extension points it uses (or, in future, the providers it implements). See App Manifest for every field.

A minimal manifest looks like this:

{
  "schemaVersion": 1,
  "version": "1.0.0",
  "scopes": [
    "catalogue.items.read",
    "catalogue.items.create"
  ],
  "oauth": {
    "redirectUrls": ["https://myapp.example.com/callback"]
  }
}

Manifest validation

When you upload a manifest, Stok.ly validates it before a release can be created or updated, and reports every problem at once. Every URL in your manifest, including your OAuth redirectUrls, is checked against the same rules used for webhook URLs:

  • HTTPS only, on port 443
  • The hostname must resolve in DNS
  • No localhost or private IP ranges
  • No loopback addresses
  • No internal hostnames
  • Must be a valid, well-formed URI
  • No credentials embedded in the URI
  • Up to 2048 characters

On the dev environment only, local URLs are allowed too. See Local development.

Your redirect URL is checked again on every OAuth request (when a user starts an install or update, and when you exchange the code), not only at upload, so a URL that passed validation but now resolves to a disallowed address (for example, through a changed DNS record) is refused at the point it's used.

The URLs your app serves content from (its worker, pages and extension frames) must also be on origins that no other developer's app uses, and can't be on Stok.ly's own domains. See URLs and origins.

Getting your credentials

Once your app is created, you'll receive:

  • A Client ID, a public identifier for your app. It is your app's id, the same one used in its Stok.ly URLs and as the audience of its session tokens.
  • A Client Secret, used to authenticate your app when exchanging an authorization code for tokens and when refreshing them. Treat this like a password, it's shown once and can't be retrieved again after you leave the page.
  • A Webhook Secret, used to check that webhooks really come from Stok.ly. See Verifying webhooks. It's also shown once.

If your client secret is lost or leaked, use Regenerate client secret on your app's page in the developer portal. The old secret stops working straight away for code exchanges and token refreshes, so update your backend with the new one. Access tokens already issued keep working until they expire, which is at most an hour.

If your webhook secret is lost or leaked, use Regenerate webhook secret. Webhooks are signed with the new secret within a few moments, so update your backend straight away.

Your listing URL

Every app has a listing in the Stok.ly marketplace. Its link is shown on your app's page in the developer portal, next to your Client ID, and works once you publish a release. It has the form

https://app.stok.ly/marketplace/{appId}

On dev, the host is https://app.dev.stok.ly.

Share this link with users (or embed it in your own onboarding) so they can install your app. The listing shows your app, the scopes you requested and what it adds to and removes from Stok.ly, and if they click Install, Stok.ly redirects to your app to finish the install. See Installing your app.

Installing your app

When a user clicks Install on your listing, Stok.ly redirects their browser to the first entry in your manifest's oauth.redirectUrls, adding two query parameters:

https://myapp.example.com/callback?code=<authorization code>&state=<JWT>

Your backend then does two things: verify state, and exchange code for tokens. Exchanging the code is what completes the install. Until your app exchanges it, the app isn't installed on the account.

Verifying state

state is a JWT signed with RS256 by Stok.ly. It tells you which account and user the install is for, and it can't be forged or altered. Verify it before you do anything else, using the same public keys as session tokens:

  • Production: https://api.stok.ly/v1/app-extensions/jwks.json, issuer https://api.stok.ly/apps.
  • Dev: https://api.dev.stok.ly/v1/app-extensions/jwks.json, issuer https://api.dev.stok.ly/apps.
ClaimValue
issthe Stok.ly issuer above
audyour app's Client ID, which is its app id
subthe Stok.ly user id of the user who installed the app
accountkeythe account the app is being installed on
app_idyour app's id
release_idthe release being installed
token_usealways "app_install"
jtia unique id for this install request
iatwhen the state was issued
expwhen the state expires, 10 minutes after iat

Check the signature (using the key that matches the token's kid), iss, aud (your own Client ID), exp, and that token_use is "app_install". You can also remember each jti you've handled, to reject a state that's replayed.

Exchanging the code

Send the code from your backend, never from the browser, because it needs your Client Secret:

POST https://api.stok.ly/v1/oauth/token

Use https://api.dev.stok.ly on dev. The endpoint is public and takes a JSON or form body with the standard OAuth fields:

FieldValue
grant_typeauthorization_code
codethe code query parameter
client_idyour app's Client ID
client_secretyour app's Client Secret
redirect_urithe redirect URL Stok.ly used, which must equal the first entry in your oauth.redirectUrls exactly

The code expires after 2 minutes and can be used once. If an exchange fails, the user has to start again from your listing URL.

A request with a missing field or an unsupported grant_type gets a 400 with a message naming the problem. A code that is wrong, expired or already used, or a wrong client_id, client_secret or redirect_uri, fails with Invalid credentials, so treat any non-2xx response as a failed exchange.

The response contains the tokens for the account:

{
  "data": {
    "accessToken": "stk.at.…",
    "refreshToken": "stk.rt.…",
    "expiry": "2026-01-01T12:00:00.000Z",
    "accountkey": "acme"
  },
  "metadata": {}
}

Before you store the tokens, check that the accountkey in the response equals the accountkey claim from state. Store the tokens securely, keyed by account. They are how your app calls the Stok.ly API for that account.

Refreshing tokens

An access token lasts 1 hour (expiry in the response). To get a new one, send the refresh token from your backend to the same endpoint:

FieldValue
grant_typerefresh_token
refresh_tokenthe account's current refresh token
client_idyour app's Client ID
client_secretyour app's Client Secret

The response holds the new accessToken, refreshToken and expiry, in the same data wrapper as the code exchange. It has no accountkey, so keep track of which account each refresh token belongs to. A refresh token lasts 28 days and is replaced every time you use it, so always store the new one and never reuse the old one.

When an account uninstalls your app, its access and refresh tokens stop working within moments, and you receive an app.uninstalled webhook if you've subscribed to it.

Example

In Node, with the jose library, following the same pattern as the session token example:

import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL("https://api.stok.ly/v1/app-extensions/jwks.json"));
const usedStates = new Set(); // use a shared store with expiry in a real app

app.get("/callback", async (req, res) => {
	const { code, state } = req.query;

	// 1. verify state
	const { payload } = await jwtVerify(state, jwks, {
		issuer: "https://api.stok.ly/apps",
		audience: process.env.STOKLY_CLIENT_ID,
		algorithms: ["RS256"],
	});
	if (payload.token_use !== "app_install") {
		throw new Error("not a Stok.ly app install state");
	}
	if (usedStates.has(payload.jti)) {
		throw new Error("install state already used");
	}
	usedStates.add(payload.jti);

	// 2. exchange the code, which completes the install
	const response = await fetch("https://api.stok.ly/v1/oauth/token", {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({
			grant_type: "authorization_code",
			code,
			client_id: process.env.STOKLY_CLIENT_ID,
			client_secret: process.env.STOKLY_CLIENT_SECRET,
			redirect_uri: "https://myapp.example.com/callback", // your first oauth.redirectUrls entry
		}),
	});
	if (!response.ok) {
		throw new Error(`code exchange failed: ${response.status}`);
	}
	const { data: tokens } = await response.json();

	// 3. the tokens must be for the account the install was started on
	if (tokens.accountkey !== payload.accountkey) {
		throw new Error("account mismatch");
	}

	// store tokens.accessToken and tokens.refreshToken securely, keyed by tokens.accountkey
	res.send(`Installed on account ${tokens.accountkey}`);
});

In Express 5 a rejected promise in a handler becomes a 500 response. In production, catch these errors and show the user a clear message.

Updating your app

To ship a change, publish a new release. Its version must be a semantic version (major.minor.patch) greater than your latest published release, or the publish is rejected. A published release never changes, so every change, including a fix, is a new version.

Accounts that have your app installed keep running the release they installed until a user with permission to install apps on that account chooses to update. Stok.ly shows an Update available tag on the app in the Apps list, and a banner with the installed and new versions on the app's page and its marketplace listing. Clicking Update shows a summary of what changes before the user confirms. Updates always go to your latest published release. There's no rollback to an earlier release.

What happens next depends on whether the new release asks for more scopes:

  • No new scopes. Stok.ly applies the update straight away without contacting your app. The account's pages, extensions, hooks and worker_url switch to the new release, and session tokens carry the new release_id. Scopes you dropped are removed from the app's access. Your existing tokens keep working.
  • New scopes. The user has to approve them first. Stok.ly redirects to your install redirect URL (the first entry in oauth.redirectUrls) with a code and state, exactly as for an install, and the state carries the new release_id. Exchange the code as you would for an install. Exchanging it completes the update. The response has fresh tokens for the same installation, and the app is not installed a second time. Your existing tokens stay valid, and the new scopes apply to them once the code is exchanged.

Your app can tell an update from an install by whether it already holds tokens for accountkey. Replace the stored tokens with the new ones.

Testing with a sandbox account

🚧

Not available yet. Sandbox accounts are planned; until then, test on an account of your own.

Creating an app will also let you create a sandbox account, a real Stok.ly account flagged for testing, with tighter limits (no real external calls, tight rate limits, emails only deliverable to your own address) so you can build and test your app without touching a live account.

What's next

  • App Manifest - every field your manifest can contain.
  • Extension Points - if your app renders UI inside Stok.ly or hooks into its actions.
  • App Bridge - the SDK your frames and worker use to talk to Stok.ly.
  • Providers - if your app implements a shipping, channel, or accounting integration (coming in a later phase).
  • Access Scopes - the full list of scopes you can request in your manifest.
  • Authentication - how installation and the OAuth flow work end to end.


Did this page help you?